From f162072200915e8f0a9b4c5bd5344a42bf1f2a2e Mon Sep 17 00:00:00 2001 From: Carlos Lapao Date: Mon, 11 Nov 2024 11:52:51 +0000 Subject: [PATCH] Major overhaul for the provider (#59) * Major overhaul for the provider - Fixed an issue where the specs where not being applied correctly if the machine was stop at some points - Fixed an issue where the specs cpu_count and memory_count where not being calculated correctly - Added the ability to use the vm datasource with the orchestrator - Added the new shared block to allow port forwarding in the the following providers, clone_vm, remote_image, vagrant_box and deploy - Updated all examples to include the new formats and the new fields - Added an output to the following providers, clone_vm, remote_image, vagrant_box showing the internal and external ips if available - Fixed an issue where if the api_port was changed after it was already deployed it would not register with the host - Added a new property to set the desired state for any of the VM providers with keep_running - Moved the resources files to use the new versioning approach for future upgrades in schemas * fix linting issues * regenerated documentation --- docs/data-sources/vm.md | 37 +- docs/index.md | 3 + docs/resources/clone_vm.md | 116 ++++- docs/resources/deploy.md | 133 ++++- docs/resources/remote_vm.md | 117 ++++- docs/resources/vagrant_box.md | 90 +++- .../parallels-desktop_vm/data-source.tf | 16 +- examples/provider/provider.tf | 3 + .../parallels-desktop_clone_vm/resource.tf | 46 +- .../parallels-desktop_deploy/resource.tf | 60 ++- .../parallels-desktop_remote_vm/resource.tf | 46 +- .../parallels-desktop_vagrant_box/resource.tf | 20 +- .../apimodels/create_reverse_proxy_host.go | 17 + .../apiclient/apimodels/orchestrator_host.go | 44 +- internal/apiclient/apimodels/reverse_proxy.go | 43 ++ internal/apiclient/apimodels/system_usage.go | 40 +- .../apiclient/apimodels/virtual_machine.go | 3 + .../apiclient/create_reverse_proxy_host.go | 45 ++ .../apiclient/delete_reverse_proxy_host.go | 44 ++ internal/apiclient/get_orchestrator_host.go | 30 ++ internal/apiclient/get_reverse_proxy_host.go | 52 ++ internal/apiclient/get_system_usage.go | 7 +- internal/apiclient/host_config.go | 1 + .../resource_models_v0.go} | 6 +- .../clone_vm/models/resource_models_v1.go | 40 ++ internal/clone_vm/resource.go | 283 ++++++++++- .../resource_schema_v0.go} | 4 +- .../clone_vm/schemas/resource_schema_v1.go | 123 +++++ internal/common/basetype_helpers.go | 37 ++ internal/common/check_required_specs.go | 4 +- .../common/ensure_machine_as_internal_ip.go | 55 +++ internal/common/ensure_machine_running.go | 18 +- internal/common/ensure_machine_stopped.go | 2 +- internal/common/specs.go | 12 +- internal/deploy/api_config_schema.go | 232 --------- internal/deploy/devops_service.go | 19 +- internal/deploy/models/common.go | 178 +++++++ internal/deploy/models/resource_models_v0.go | 45 ++ internal/deploy/models/resource_models_v1.go | 99 ++++ internal/deploy/models/resource_models_v2.go | 142 ++++++ internal/deploy/resource.go | 465 ++++++++++++------ internal/deploy/resource_models.go | 305 ------------ .../deploy/schemas/api_config_schema_v0.go | 115 +++++ .../deploy/schemas/api_config_schema_v1.go | 121 +++++ .../deploy/schemas/api_config_schema_v2.go | 129 +++++ internal/deploy/schemas/resource_schema_v0.go | 86 ++++ internal/deploy/schemas/resource_schema_v1.go | 86 ++++ .../resource_schema_v2.go} | 96 +--- internal/deploy/schemas/schema_names.go | 3 + internal/helpers/client.go | 3 + .../resource_models_v0.go} | 4 +- .../remoteimage/models/resource_models_v1.go | 44 ++ internal/remoteimage/resource.go | 403 +++++++++++++-- .../resource_schema_v0.go} | 5 +- .../remoteimage/schemas/resource_schema_v1.go | 146 ++++++ internal/schemas/orchestrator/main.go | 25 +- internal/schemas/orchestrator/schema.go | 72 +-- internal/schemas/reverseproxy/common.go | 3 + .../schemas/reverseproxy/cors_schema_v0.go | 48 ++ .../schemas/reverseproxy/host_schema_v0.go | 33 ++ .../reverseproxy/http_route_schema_v0.go | 84 ++++ internal/schemas/reverseproxy/models.go | 372 ++++++++++++++ internal/schemas/reverseproxy/operations.go | 341 +++++++++++++ .../reverseproxy/tcp_route_schema_v0.go | 42 ++ .../schemas/reverseproxy/tls_schema_v0.go | 27 + internal/schemas/sshconnection/schema.go | 64 +-- .../resource_models_v0.go} | 4 +- .../vagrantbox/models/resource_models_v1.go | 43 ++ internal/vagrantbox/resource.go | 231 ++++++++- .../resource_schema_v0.go} | 4 +- .../vagrantbox/schemas/resource_schema_v1.go | 151 ++++++ internal/virtualmachine/datasource.go | 42 +- .../datasource_models_v0.go} | 8 +- .../models/datasource_models_v1.go | 31 ++ .../datasource_schema_v0.go} | 25 +- .../schemas/datasource_schema_v1.go | 88 ++++ .../resource_model_v0.go} | 4 +- .../models/resource_model_v1.go | 17 + internal/virtualmachinestate/resource.go | 65 ++- .../resource_schema_v0.go} | 4 +- .../schemas/resource_schema_v1.go | 65 +++ 81 files changed, 5151 insertions(+), 1065 deletions(-) create mode 100644 internal/apiclient/apimodels/create_reverse_proxy_host.go create mode 100644 internal/apiclient/apimodels/reverse_proxy.go create mode 100644 internal/apiclient/create_reverse_proxy_host.go create mode 100644 internal/apiclient/delete_reverse_proxy_host.go create mode 100644 internal/apiclient/get_reverse_proxy_host.go rename internal/clone_vm/{resource_models.go => models/resource_models_v0.go} (94%) create mode 100644 internal/clone_vm/models/resource_models_v1.go rename internal/clone_vm/{resource_schema.go => schemas/resource_schema_v0.go} (98%) create mode 100644 internal/clone_vm/schemas/resource_schema_v1.go create mode 100644 internal/common/basetype_helpers.go create mode 100644 internal/common/ensure_machine_as_internal_ip.go delete mode 100644 internal/deploy/api_config_schema.go create mode 100644 internal/deploy/models/common.go create mode 100644 internal/deploy/models/resource_models_v0.go create mode 100644 internal/deploy/models/resource_models_v1.go create mode 100644 internal/deploy/models/resource_models_v2.go delete mode 100644 internal/deploy/resource_models.go create mode 100644 internal/deploy/schemas/api_config_schema_v0.go create mode 100644 internal/deploy/schemas/api_config_schema_v1.go create mode 100644 internal/deploy/schemas/api_config_schema_v2.go create mode 100644 internal/deploy/schemas/resource_schema_v0.go create mode 100644 internal/deploy/schemas/resource_schema_v1.go rename internal/deploy/{resource_schema.go => schemas/resource_schema_v2.go} (54%) create mode 100644 internal/deploy/schemas/schema_names.go rename internal/remoteimage/{resource_models.go => models/resource_models_v0.go} (97%) create mode 100644 internal/remoteimage/models/resource_models_v1.go rename internal/remoteimage/{resource_schema.go => schemas/resource_schema_v0.go} (97%) create mode 100644 internal/remoteimage/schemas/resource_schema_v1.go create mode 100644 internal/schemas/reverseproxy/common.go create mode 100644 internal/schemas/reverseproxy/cors_schema_v0.go create mode 100644 internal/schemas/reverseproxy/host_schema_v0.go create mode 100644 internal/schemas/reverseproxy/http_route_schema_v0.go create mode 100644 internal/schemas/reverseproxy/models.go create mode 100644 internal/schemas/reverseproxy/operations.go create mode 100644 internal/schemas/reverseproxy/tcp_route_schema_v0.go create mode 100644 internal/schemas/reverseproxy/tls_schema_v0.go rename internal/vagrantbox/{resource_models.go => models/resource_models_v0.go} (97%) create mode 100644 internal/vagrantbox/models/resource_models_v1.go rename internal/vagrantbox/{resource_schema.go => schemas/resource_schema_v0.go} (98%) create mode 100644 internal/vagrantbox/schemas/resource_schema_v1.go rename internal/virtualmachine/{datasource_models.go => models/datasource_models_v0.go} (88%) create mode 100644 internal/virtualmachine/models/datasource_models_v1.go rename internal/virtualmachine/{data_source_schema.go => schemas/datasource_schema_v0.go} (55%) create mode 100644 internal/virtualmachine/schemas/datasource_schema_v1.go rename internal/virtualmachinestate/{resource_models.go => models/resource_model_v0.go} (87%) create mode 100644 internal/virtualmachinestate/models/resource_model_v1.go rename internal/virtualmachinestate/{resource_schema.go => schemas/resource_schema_v0.go} (94%) create mode 100644 internal/virtualmachinestate/schemas/resource_schema_v1.go diff --git a/docs/data-sources/vm.md b/docs/data-sources/vm.md index eaaea0f..9e19718 100644 --- a/docs/data-sources/vm.md +++ b/docs/data-sources/vm.md @@ -14,8 +14,20 @@ Virtual Machine Data Source ```terraform data "parallels-desktop_vm" "example" { - host = "http://example.com:8080" + # You can only use one of the following options + # Use the host if you need to connect directly to a host + host = "http:#example.com:8080" + # Use the orchestrator if you need to connect to a Parallels Orchestrator + orchestrator = "https:#orchestrator.example.com:443" + + # The authenticator block for authenticating to the API, either to the host or orchestrator + authenticator { + username = "john.doe" + password = "my-password" + } + + # The filter block to filter the VMs filter { field_name = "name" value = "exampe-vm" @@ -26,14 +38,12 @@ data "parallels-desktop_vm" "example" { ## Schema -### Required - -- `host` (String) - ### Optional - `authenticator` (Block, Optional) Authenticator block, this is used to authenticate with the Parallels Desktop API, if empty it will try to use the root password (see [below for nested schema](#nestedblock--authenticator)) - `filter` (Block, Optional) Filter block, this is used to filter data sources (see [below for nested schema](#nestedblock--filter)) +- `host` (String) Parallels Desktop DevOps Host +- `orchestrator` (String) Parallels Desktop DevOps Orchestrator ### Read-Only @@ -67,10 +77,13 @@ Optional: Read-Only: -- `description` (String) -- `home` (String) -- `host_ip` (String) -- `id` (String) -- `name` (String) -- `os_type` (String) -- `state` (String) +- `description` (String) The description of the virtual machine +- `external_ip` (String) VM external IP address +- `home` (String) The path to the virtual machine home directory +- `host_ip` (String) The IP address of the host machine +- `id` (String) The unique identifier of the virtual machine +- `internal_ip` (String) VM internal IP address +- `name` (String) The name of the virtual machine +- `orchestrator_host_id` (String) Orchestrator Host Id if the VM is running in an orchestrator +- `os_type` (String) The type of the operating system installed on the virtual machine +- `state` (String) The state of the virtual machine diff --git a/docs/index.md b/docs/index.md index 7095140..aeb5b46 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,9 @@ You can also join our community on [Discord](https://discord.gg/aFsrjbkN) channe ```terraform provider "parallels-desktop" { license = "xxxx-xxxx-xxxx-xxxx" + # Optional, will disable TLS validation when doing calls to the API using HTTPS + # this is useful when the API is using a self-signed certificate + disable_tls_validation = true } ``` diff --git a/docs/resources/clone_vm.md b/docs/resources/clone_vm.md index 962f573..b6ce282 100644 --- a/docs/resources/clone_vm.md +++ b/docs/resources/clone_vm.md @@ -14,7 +14,12 @@ Parallels Desktop Clone VM resource ```terraform data "parallels-desktop_vm" "example" { - host = "https://example.com:8080" + host = "https:#example.com:8080" + + authenticator { + username = "john.doe" + password = "my-password" + } filter { field_name = "name" @@ -24,16 +29,29 @@ data "parallels-desktop_vm" "example" { } resource "parallels-desktop_clone_vm" "example" { - host = "https://example.com:8080" + # You can only use one of the following options + + # Use the host if you need to connect directly to a host + host = "http://example.com:8080" + # Use the orchestrator if you need to connect to a Parallels Orchestrator + orchestrator = "https://orchestrator.example.com:443" + name = "example-vm" owner = "example" base_vm_id = data.parallels-desktop_vm.example.machines[count.index].id path = "/some/folder/path" + # The authenticator block for authenticating to the API, either to the host or orchestrator + # in this case we are using the API key authenticator { api_key = "some api key" } + # The configuration for the VM + config { + start_headless = true + } + # this will allow you to fine grain the configuration of the VM # you can pass any command that is compatible with the prlctl command # directly to the VM @@ -56,6 +74,24 @@ resource "parallels-desktop_clone_vm" "example" { force_changes = true + # this flag will set the desired state for the VM + # if it is set to true it will keep the VM running otherwise it will stop it + # by default it is set to true, so all VMs will be running + keep_running = true + + # This will contain the configuration for the port forwarding reverse proxy + # in this case we are opening a port to any part in the host, it will not be linked to any + # specific vm or container. by default it will listen on 0.0.0.0 (all interfaces) + # and the target host will also be 0.0.0.0 (all interfaces) so it will be open to the world + # use + reverse_proxy_host { + port = "2022" + + tcp_route { + target_port = "22" + } + } + # This will contain the configuration for the shared folders shared_folder { name = "user_download_folder" @@ -66,7 +102,7 @@ resource "parallels-desktop_clone_vm" "example" { # allowing you to run any command on the VM after it has been deployed # you can have multiple lines and they will be executed in order post_processor_script { - // Retry the script 4 times with 10 seconds between each attempt + # Retry the script 4 times with 10 seconds between each attempt retry { attempts = 4 wait_between_attempts = "10s" @@ -80,7 +116,7 @@ resource "parallels-desktop_clone_vm" "example" { # This is a special block that will allow you to undo any changes your scripts have done # if you are destroying a VM, like unregistering from a service where the VM was registered on_destroy_script { - // Retry the script 4 times with 10 seconds between each attempt + # Retry the script 4 times with 10 seconds between each attempt retry { attempts = 4 wait_between_attempts = "10s" @@ -113,14 +149,18 @@ resource "parallels-desktop_clone_vm" "example" { - `owner` (String) Virtual Machine owner - `post_processor_script` (Block List) Run any script after the virtual machine is created (see [below for nested schema](#nestedblock--post_processor_script)) - `prlctl` (Block List) Virtual Machine config block, this is used set some of the most common settings for a VM (see [below for nested schema](#nestedblock--prlctl)) -- `run_after_create` (Boolean) Run after create, this will make the VM to run after creation +- `reverse_proxy_host` (Block List) Parallels Desktop DevOps Reverse Proxy configuration (see [below for nested schema](#nestedblock--reverse_proxy_host)) +- `run_after_create` (Boolean, Deprecated) Run after create, this will make the VM to run after creation - `shared_folder` (Block List) Shared Folders Block, this is used to share folders with the virtual machine (see [below for nested schema](#nestedblock--shared_folder)) - `specs` (Block, Optional) Virtual Machine Specs block, this is used to set the specs of the virtual machine (see [below for nested schema](#nestedblock--specs)) - `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) ### Read-Only +- `external_ip` (String) VM external IP address - `id` (String) Virtual Machine Id +- `internal_ip` (String) VM internal IP address +- `keep_running` (Boolean) This will keep the VM running after the terraform apply - `os_type` (String) Virtual Machine OS type @@ -231,6 +271,72 @@ Optional: + +### Nested Schema for `reverse_proxy_host` + +Required: + +- `port` (String) Reverse proxy port + +Optional: + +- `cors` (Block, Optional) Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--cors)) +- `host` (String) Reverse proxy host +- `http_routes` (Block List) Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--http_routes)) +- `tcp_route` (Block, Optional) Parallels Desktop DevOps Reverse Proxy TCP Route configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--tcp_route)) +- `tls` (Block, Optional) Parallels Desktop DevOps Reverse Proxy Http Route TLS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--tls)) + +Read-Only: + +- `id` (String) Reverse proxy Host id + + +### Nested Schema for `reverse_proxy_host.cors` + +Optional: + +- `allowed_headers` (List of String) Allowed headers +- `allowed_methods` (List of String) Allowed methods +- `allowed_origins` (List of String) Allowed origins +- `enabled` (Boolean) Enable CORS + + + +### Nested Schema for `reverse_proxy_host.http_routes` + +Optional: + +- `path` (String) Reverse proxy HTTP Route path +- `pattern` (String) Reverse proxy HTTP Route pattern +- `request_headers` (Map of String) Reverse proxy HTTP Route request headers +- `response_headers` (Map of String) Reverse proxy HTTP Route response headers +- `schema` (String) Reverse proxy HTTP Route schema +- `target_host` (String) Reverse proxy HTTP Route target host +- `target_port` (String) Reverse proxy HTTP Route target port +- `target_vm_id` (String) Reverse proxy HTTP Route target VM id + + + +### Nested Schema for `reverse_proxy_host.tcp_route` + +Optional: + +- `target_host` (String) Reverse proxy host +- `target_port` (String) Reverse proxy port +- `target_vm_id` (String) Reverse proxy target VM ID + + + +### Nested Schema for `reverse_proxy_host.tls` + +Optional: + +- `certificate` (String) TLS Certificate +- `enabled` (Boolean) Enable TLS +- `private_key` (String) TLS Private Key + + + ### Nested Schema for `shared_folder` diff --git a/docs/resources/deploy.md b/docs/resources/deploy.md index cac7e01..d0fe16b 100644 --- a/docs/resources/deploy.md +++ b/docs/resources/deploy.md @@ -17,25 +17,59 @@ resource "parallels-desktop_deploy" "example" { # This will contain the configuration for the Parallels Desktop API api_config { - port = "8080" - prefix = "/api" - log_level = "info" - mode = "api" - devops_version = "latest" - root_password = "VerySecretPassword" - hmac_secret = "VerySecretLongStringForHMAC" - encryption_rsa_key = "base64 encoded rsa key" - enable_tls = true - tls_port = "8443" - tls_certificate = "base64 encoded tls cert" - tls_private_key = "base64 encoded tls key" - disable_catalog_caching = false + port = "8080" + prefix = "/api" + # This will set the log level for the API + log_level = "info" + # This will enable logging for the API + enable_logging = true + # This will set the mode for the API, you can use either api or orchestrator. by default it will be api + mode = "api" + # you can force any version of the devops api, if you leave it empty it will use the latest version + # but it will not automatically update to the latest version, that would need a manual step + devops_version = "latest" + # This will set the password for the default root user + root_password = "VerySecretPassword" + # This will enable the api to use the hmac secret for the authentication + hmac_secret = "VerySecretLongStringForHMAC" + # This will enable the api to use the rsa key for encryption of the database file + # we strongly advise you to use this feature for security reasons + encryption_rsa_key = "base64 encoded rsa key" + # This will enable the api to use the tls certificate + enable_tls = true + # This will enable the tls port + tls_port = "8443" + # This will enable the tls certificate + tls_certificate = "base64 encoded tls cert" + # This will enable the tls private key + tls_private_key = "base64 encoded tls key" + # This will enable the catalog caching, this will cache the catalog in the host + disable_catalog_caching = false + # This will enable the orchestrator resources, this will enable the host to use the orchestrator use_orchestrator_resources = false + # This will enable the port forwarding reverse proxy in the host, you will need to set the + # port_forwarding block to configure the ports in the deploy or any other provider + enable_port_forwarding = false + # This will allow more fine tune of the api configuration, you can pass any compatible environment + # variable environment_variables = { "key" = "value" } } + # This will contain the configuration for the port forwarding reverse proxy + # in this case we are opening a port to any part in the host, it will not be linked to any + # specific vm or container. by default it will listen on 0.0.0.0 (all interfaces) + # and the target host will also be 0.0.0.0 (all interfaces) so it will be open to the world + # use + reverse_proxy_host { + port = "2022" + + tcp_route { + target_port = "22" + } + } + # This will contain the configuration for the Parallels Desktop Orchestrator # and how to register this instance with it orchestrator_registration { @@ -78,6 +112,7 @@ resource "parallels-desktop_deploy" "example" { - `api_config` (Block, Optional) Parallels Desktop DevOps configuration (see [below for nested schema](#nestedblock--api_config)) - `install_local` (Boolean) Deploy Parallels Desktop in the local machine, this will ignore the need to connect to a remote machine - `orchestrator_registration` (Block, Optional) Orchestrator connection details (see [below for nested schema](#nestedblock--orchestrator_registration)) +- `reverse_proxy_host` (Block List) Parallels Desktop DevOps Reverse Proxy configuration (see [below for nested schema](#nestedblock--reverse_proxy_host)) - `ssh_connection` (Block, Optional) Host connection details (see [below for nested schema](#nestedblock--ssh_connection)) ### Read-Only @@ -87,8 +122,12 @@ resource "parallels-desktop_deploy" "example" { - `current_packer_version` (String) Current version of Hashicorp Packer - `current_vagrant_version` (String) Current version of Hashicorp Vagrant - `current_version` (String) Current version of Parallels Desktop +- `external_ip` (String) External IP address - `installed_dependencies` (List of String) List of installed dependencies +- `is_registered_in_orchestrator` (Boolean) Is this host registered in the orchestrator - `license` (Object) Parallels Desktop license (see [below for nested schema](#nestedatt--license)) +- `orchestrator_host` (String) Orchestrator host ID +- `orchestrator_host_id` (String) Orchestrator host ID ### Nested Schema for `api_config` @@ -98,6 +137,7 @@ Optional: - `devops_version` (String) Parallels Desktop DevOps version to install, if empty the latest will be installed - `disable_catalog_caching` (Boolean) Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog - `enable_logging` (Boolean) Enable logging +- `enable_port_forwarding` (Boolean) Enable inbuilt reverse proxy for port forwarding - `enable_tls` (Boolean) Parallels Desktop DevOps enable TLS - `encryption_rsa_key` (String, Sensitive) Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest - `environment_variables` (Map of String) Environment variables that can be used in the DevOps service, please see documentation to see which variables are available @@ -114,6 +154,7 @@ Optional: - `tls_port` (String) Parallels Desktop DevOps TLS port - `tls_private_key` (String, Sensitive) Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string - `token_duration_minutes` (String) JWT Token duration in minutes +- `use_latest_beta` (Boolean) Enables the use of the latest beta - `use_orchestrator_resources` (Boolean) Use orchestrator resources @@ -166,6 +207,72 @@ Optional: + +### Nested Schema for `reverse_proxy_host` + +Required: + +- `port` (String) Reverse proxy port + +Optional: + +- `cors` (Block, Optional) Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--cors)) +- `host` (String) Reverse proxy host +- `http_routes` (Block List) Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--http_routes)) +- `tcp_route` (Block, Optional) Parallels Desktop DevOps Reverse Proxy TCP Route configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--tcp_route)) +- `tls` (Block, Optional) Parallels Desktop DevOps Reverse Proxy Http Route TLS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--tls)) + +Read-Only: + +- `id` (String) Reverse proxy Host id + + +### Nested Schema for `reverse_proxy_host.cors` + +Optional: + +- `allowed_headers` (List of String) Allowed headers +- `allowed_methods` (List of String) Allowed methods +- `allowed_origins` (List of String) Allowed origins +- `enabled` (Boolean) Enable CORS + + + +### Nested Schema for `reverse_proxy_host.http_routes` + +Optional: + +- `path` (String) Reverse proxy HTTP Route path +- `pattern` (String) Reverse proxy HTTP Route pattern +- `request_headers` (Map of String) Reverse proxy HTTP Route request headers +- `response_headers` (Map of String) Reverse proxy HTTP Route response headers +- `schema` (String) Reverse proxy HTTP Route schema +- `target_host` (String) Reverse proxy HTTP Route target host +- `target_port` (String) Reverse proxy HTTP Route target port +- `target_vm_id` (String) Reverse proxy HTTP Route target VM id + + + +### Nested Schema for `reverse_proxy_host.tcp_route` + +Optional: + +- `target_host` (String) Reverse proxy host +- `target_port` (String) Reverse proxy port +- `target_vm_id` (String) Reverse proxy target VM ID + + + +### Nested Schema for `reverse_proxy_host.tls` + +Optional: + +- `certificate` (String) TLS Certificate +- `enabled` (Boolean) Enable TLS +- `private_key` (String) TLS Private Key + + + ### Nested Schema for `ssh_connection` diff --git a/docs/resources/remote_vm.md b/docs/resources/remote_vm.md index eaa124e..3a7ce66 100644 --- a/docs/resources/remote_vm.md +++ b/docs/resources/remote_vm.md @@ -14,13 +14,25 @@ Parallels Virtual Machine State Resource ```terraform resource "parallels-desktop_remote_vm" "example_box" { - host = "https://example.com:8080" - name = "example-vm" - owner = "example" - catalog_id = "example-catalog-id" - version = "v1" - host_connection = "host=user:VerySecretPassword@example.com" - path = "/Users/example/Parallels" + # You can only use one of the following options + + # Use the host if you need to connect directly to a host + host = "http://example.com:8080" + # Use the orchestrator if you need to connect to a Parallels Orchestrator + orchestrator = "https://orchestrator.example.com:443" + + # The name of the VM + name = "example-vm" + # The owner of the VM, otherwise it will be set as root + owner = "example" + # The catalog id of the VM from the catalog provider + catalog_id = "example-catalog-id" + # The version of the VM from the catalog provider + version = "v1" + # The connection to the catalog provider + catalog_connection = "host=user:VerySecretPassword@example.com" + # The path where the VM will be stored + path = "/Users/example/Parallels" # This will tell how should we authenticate with the host API # you can either use it or leave it empty, if left empty then @@ -44,6 +56,24 @@ resource "parallels-desktop_remote_vm" "example_box" { memory_size = "2048" } + # this flag will set the desired state for the VM + # if it is set to true it will keep the VM running otherwise it will stop it + # by default it is set to true, so all VMs will be running + keep_running = true + + # This will contain the configuration for the port forwarding reverse proxy + # in this case we are opening a port to any part in the host, it will not be linked to any + # specific vm or container. by default it will listen on 0.0.0.0 (all interfaces) + # and the target host will also be 0.0.0.0 (all interfaces) so it will be open to the world + # use + reverse_proxy_host { + port = "2022" + + tcp_route { + target_port = "22" + } + } + # this will allow you to fine grain the configuration of the VM # you can pass any command that is compatible with the prlctl command # directly to the VM @@ -120,12 +150,14 @@ resource "parallels-desktop_remote_vm" "example_box" { - `config` (Block, Optional) Virtual Machine config block, this is used set some of the most common settings for a VM (see [below for nested schema](#nestedblock--config)) - `force_changes` (Boolean) Force changes, this will force the VM to be stopped and started again - `host` (String) Parallels Desktop DevOps Host +- `keep_running` (Boolean) This will keep the VM running after the terraform apply - `on_destroy_script` (Block List) Run any script after the virtual machine is created (see [below for nested schema](#nestedblock--on_destroy_script)) - `orchestrator` (String) Parallels Desktop DevOps Orchestrator - `owner` (String) Virtual Machine owner - `post_processor_script` (Block List) Run any script after the virtual machine is created (see [below for nested schema](#nestedblock--post_processor_script)) - `prlctl` (Block List) Virtual Machine config block, this is used set some of the most common settings for a VM (see [below for nested schema](#nestedblock--prlctl)) -- `run_after_create` (Boolean) Run after create, this will make the VM to run after creation +- `reverse_proxy_host` (Block List) Parallels Desktop DevOps Reverse Proxy configuration (see [below for nested schema](#nestedblock--reverse_proxy_host)) +- `run_after_create` (Boolean, Deprecated) Run after create, this will make the VM to run after creation - `shared_folder` (Block List) Shared Folders Block, this is used to share folders with the virtual machine (see [below for nested schema](#nestedblock--shared_folder)) - `specs` (Block, Optional) Virtual Machine Specs block, this is used to set the specs of the virtual machine (see [below for nested schema](#nestedblock--specs)) - `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) @@ -133,7 +165,10 @@ resource "parallels-desktop_remote_vm" "example_box" { ### Read-Only +- `external_ip` (String) VM external IP address - `id` (String) Virtual Machine Id +- `internal_ip` (String) VM internal IP address +- `orchestrator_host_id` (String) Orchestrator Host Id if the VM is running in an orchestrator - `os_type` (String) Virtual Machine OS type @@ -244,6 +279,72 @@ Optional: + +### Nested Schema for `reverse_proxy_host` + +Required: + +- `port` (String) Reverse proxy port + +Optional: + +- `cors` (Block, Optional) Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--cors)) +- `host` (String) Reverse proxy host +- `http_routes` (Block List) Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--http_routes)) +- `tcp_route` (Block, Optional) Parallels Desktop DevOps Reverse Proxy TCP Route configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--tcp_route)) +- `tls` (Block, Optional) Parallels Desktop DevOps Reverse Proxy Http Route TLS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--tls)) + +Read-Only: + +- `id` (String) Reverse proxy Host id + + +### Nested Schema for `reverse_proxy_host.cors` + +Optional: + +- `allowed_headers` (List of String) Allowed headers +- `allowed_methods` (List of String) Allowed methods +- `allowed_origins` (List of String) Allowed origins +- `enabled` (Boolean) Enable CORS + + + +### Nested Schema for `reverse_proxy_host.http_routes` + +Optional: + +- `path` (String) Reverse proxy HTTP Route path +- `pattern` (String) Reverse proxy HTTP Route pattern +- `request_headers` (Map of String) Reverse proxy HTTP Route request headers +- `response_headers` (Map of String) Reverse proxy HTTP Route response headers +- `schema` (String) Reverse proxy HTTP Route schema +- `target_host` (String) Reverse proxy HTTP Route target host +- `target_port` (String) Reverse proxy HTTP Route target port +- `target_vm_id` (String) Reverse proxy HTTP Route target VM id + + + +### Nested Schema for `reverse_proxy_host.tcp_route` + +Optional: + +- `target_host` (String) Reverse proxy host +- `target_port` (String) Reverse proxy port +- `target_vm_id` (String) Reverse proxy target VM ID + + + +### Nested Schema for `reverse_proxy_host.tls` + +Optional: + +- `certificate` (String) TLS Certificate +- `enabled` (Boolean) Enable TLS +- `private_key` (String) TLS Private Key + + + ### Nested Schema for `shared_folder` diff --git a/docs/resources/vagrant_box.md b/docs/resources/vagrant_box.md index 65c39be..685c9e0 100644 --- a/docs/resources/vagrant_box.md +++ b/docs/resources/vagrant_box.md @@ -49,6 +49,24 @@ resource "parallels-desktop_remote_vm" "example_vagrant_file" { memory_size = "2048" } + # this flag will set the desired state for the VM + # if it is set to true it will keep the VM running otherwise it will stop it + # by default it is set to true, so all VMs will be running + keep_running = true + + # This will contain the configuration for the port forwarding reverse proxy + # in this case we are opening a port to any part in the host, it will not be linked to any + # specific vm or container. by default it will listen on 0.0.0.0 (all interfaces) + # and the target host will also be 0.0.0.0 (all interfaces) so it will be open to the world + # use + reverse_proxy_host { + port = "2022" + + tcp_route { + target_port = "22" + } + } + # this will allow you to fine grain the configuration of the VM # you can pass any command that is compatible with the prlctl command # directly to the VM @@ -126,12 +144,14 @@ resource "parallels-desktop_remote_vm" "example_vagrant_file" { - `custom_vagrant_config` (String) Custom Vagrant config - `force_changes` (Boolean) Force changes, this will force the VM to be stopped and started again - `host` (String) Parallels Desktop DevOps Host +- `keep_running` (Boolean) This will keep the VM running after the terraform apply - `on_destroy_script` (Block List) Run any script after the virtual machine is created (see [below for nested schema](#nestedblock--on_destroy_script)) - `orchestrator` (String) Parallels Desktop DevOps Orchestrator - `owner` (String) Virtual Machine owner - `post_processor_script` (Block List) Run any script after the virtual machine is created (see [below for nested schema](#nestedblock--post_processor_script)) - `prlctl` (Block List) Virtual Machine config block, this is used set some of the most common settings for a VM (see [below for nested schema](#nestedblock--prlctl)) -- `run_after_create` (Boolean) Run after create +- `reverse_proxy_host` (Block List) Parallels Desktop DevOps Reverse Proxy configuration (see [below for nested schema](#nestedblock--reverse_proxy_host)) +- `run_after_create` (Boolean, Deprecated) Run after create - `shared_folder` (Block List) Shared Folders Block, this is used to share folders with the virtual machine (see [below for nested schema](#nestedblock--shared_folder)) - `specs` (Block, Optional) Virtual Machine Specs block, this is used to set the specs of the virtual machine (see [below for nested schema](#nestedblock--specs)) - `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) @@ -139,7 +159,9 @@ resource "parallels-desktop_remote_vm" "example_vagrant_file" { ### Read-Only +- `external_ip` (String) VM external IP address - `id` (String) Virtual Machine Id +- `internal_ip` (String) VM internal IP address - `os_type` (String) Virtual Machine OS type @@ -250,6 +272,72 @@ Optional: + +### Nested Schema for `reverse_proxy_host` + +Required: + +- `port` (String) Reverse proxy port + +Optional: + +- `cors` (Block, Optional) Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--cors)) +- `host` (String) Reverse proxy host +- `http_routes` (Block List) Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--http_routes)) +- `tcp_route` (Block, Optional) Parallels Desktop DevOps Reverse Proxy TCP Route configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--tcp_route)) +- `tls` (Block, Optional) Parallels Desktop DevOps Reverse Proxy Http Route TLS configuration (see [below for nested schema](#nestedblock--reverse_proxy_host--tls)) + +Read-Only: + +- `id` (String) Reverse proxy Host id + + +### Nested Schema for `reverse_proxy_host.cors` + +Optional: + +- `allowed_headers` (List of String) Allowed headers +- `allowed_methods` (List of String) Allowed methods +- `allowed_origins` (List of String) Allowed origins +- `enabled` (Boolean) Enable CORS + + + +### Nested Schema for `reverse_proxy_host.http_routes` + +Optional: + +- `path` (String) Reverse proxy HTTP Route path +- `pattern` (String) Reverse proxy HTTP Route pattern +- `request_headers` (Map of String) Reverse proxy HTTP Route request headers +- `response_headers` (Map of String) Reverse proxy HTTP Route response headers +- `schema` (String) Reverse proxy HTTP Route schema +- `target_host` (String) Reverse proxy HTTP Route target host +- `target_port` (String) Reverse proxy HTTP Route target port +- `target_vm_id` (String) Reverse proxy HTTP Route target VM id + + + +### Nested Schema for `reverse_proxy_host.tcp_route` + +Optional: + +- `target_host` (String) Reverse proxy host +- `target_port` (String) Reverse proxy port +- `target_vm_id` (String) Reverse proxy target VM ID + + + +### Nested Schema for `reverse_proxy_host.tls` + +Optional: + +- `certificate` (String) TLS Certificate +- `enabled` (Boolean) Enable TLS +- `private_key` (String) TLS Private Key + + + ### Nested Schema for `shared_folder` diff --git a/examples/data-sources/parallels-desktop_vm/data-source.tf b/examples/data-sources/parallels-desktop_vm/data-source.tf index 5ecb6db..8789b52 100644 --- a/examples/data-sources/parallels-desktop_vm/data-source.tf +++ b/examples/data-sources/parallels-desktop_vm/data-source.tf @@ -1,8 +1,20 @@ data "parallels-desktop_vm" "example" { - host = "http://example.com:8080" + # You can only use one of the following options + # Use the host if you need to connect directly to a host + host = "http:#example.com:8080" + # Use the orchestrator if you need to connect to a Parallels Orchestrator + orchestrator = "https:#orchestrator.example.com:443" + + # The authenticator block for authenticating to the API, either to the host or orchestrator + authenticator { + username = "john.doe" + password = "my-password" + } + + # The filter block to filter the VMs filter { field_name = "name" value = "exampe-vm" } -} \ No newline at end of file +} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 2c69192..bad96d0 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -1,3 +1,6 @@ provider "parallels-desktop" { license = "xxxx-xxxx-xxxx-xxxx" + # Optional, will disable TLS validation when doing calls to the API using HTTPS + # this is useful when the API is using a self-signed certificate + disable_tls_validation = true } diff --git a/examples/resources/parallels-desktop_clone_vm/resource.tf b/examples/resources/parallels-desktop_clone_vm/resource.tf index 192c956..512a137 100644 --- a/examples/resources/parallels-desktop_clone_vm/resource.tf +++ b/examples/resources/parallels-desktop_clone_vm/resource.tf @@ -1,5 +1,10 @@ data "parallels-desktop_vm" "example" { - host = "https://example.com:8080" + host = "https:#example.com:8080" + + authenticator { + username = "john.doe" + password = "my-password" + } filter { field_name = "name" @@ -9,16 +14,29 @@ data "parallels-desktop_vm" "example" { } resource "parallels-desktop_clone_vm" "example" { - host = "https://example.com:8080" + # You can only use one of the following options + + # Use the host if you need to connect directly to a host + host = "http://example.com:8080" + # Use the orchestrator if you need to connect to a Parallels Orchestrator + orchestrator = "https://orchestrator.example.com:443" + name = "example-vm" owner = "example" base_vm_id = data.parallels-desktop_vm.example.machines[count.index].id path = "/some/folder/path" + # The authenticator block for authenticating to the API, either to the host or orchestrator + # in this case we are using the API key authenticator { api_key = "some api key" } + # The configuration for the VM + config { + start_headless = true + } + # this will allow you to fine grain the configuration of the VM # you can pass any command that is compatible with the prlctl command # directly to the VM @@ -41,6 +59,24 @@ resource "parallels-desktop_clone_vm" "example" { force_changes = true + # this flag will set the desired state for the VM + # if it is set to true it will keep the VM running otherwise it will stop it + # by default it is set to true, so all VMs will be running + keep_running = true + + # This will contain the configuration for the port forwarding reverse proxy + # in this case we are opening a port to any part in the host, it will not be linked to any + # specific vm or container. by default it will listen on 0.0.0.0 (all interfaces) + # and the target host will also be 0.0.0.0 (all interfaces) so it will be open to the world + # use + reverse_proxy_host { + port = "2022" + + tcp_route { + target_port = "22" + } + } + # This will contain the configuration for the shared folders shared_folder { name = "user_download_folder" @@ -51,7 +87,7 @@ resource "parallels-desktop_clone_vm" "example" { # allowing you to run any command on the VM after it has been deployed # you can have multiple lines and they will be executed in order post_processor_script { - // Retry the script 4 times with 10 seconds between each attempt + # Retry the script 4 times with 10 seconds between each attempt retry { attempts = 4 wait_between_attempts = "10s" @@ -65,7 +101,7 @@ resource "parallels-desktop_clone_vm" "example" { # This is a special block that will allow you to undo any changes your scripts have done # if you are destroying a VM, like unregistering from a service where the VM was registered on_destroy_script { - // Retry the script 4 times with 10 seconds between each attempt + # Retry the script 4 times with 10 seconds between each attempt retry { attempts = 4 wait_between_attempts = "10s" @@ -75,4 +111,4 @@ resource "parallels-desktop_clone_vm" "example" { "rm -rf /tmp/*" ] } -} \ No newline at end of file +} diff --git a/examples/resources/parallels-desktop_deploy/resource.tf b/examples/resources/parallels-desktop_deploy/resource.tf index 16f5169..545e90a 100644 --- a/examples/resources/parallels-desktop_deploy/resource.tf +++ b/examples/resources/parallels-desktop_deploy/resource.tf @@ -2,25 +2,59 @@ resource "parallels-desktop_deploy" "example" { # This will contain the configuration for the Parallels Desktop API api_config { - port = "8080" - prefix = "/api" - log_level = "info" - mode = "api" - devops_version = "latest" - root_password = "VerySecretPassword" - hmac_secret = "VerySecretLongStringForHMAC" - encryption_rsa_key = "base64 encoded rsa key" - enable_tls = true - tls_port = "8443" - tls_certificate = "base64 encoded tls cert" - tls_private_key = "base64 encoded tls key" - disable_catalog_caching = false + port = "8080" + prefix = "/api" + # This will set the log level for the API + log_level = "info" + # This will enable logging for the API + enable_logging = true + # This will set the mode for the API, you can use either api or orchestrator. by default it will be api + mode = "api" + # you can force any version of the devops api, if you leave it empty it will use the latest version + # but it will not automatically update to the latest version, that would need a manual step + devops_version = "latest" + # This will set the password for the default root user + root_password = "VerySecretPassword" + # This will enable the api to use the hmac secret for the authentication + hmac_secret = "VerySecretLongStringForHMAC" + # This will enable the api to use the rsa key for encryption of the database file + # we strongly advise you to use this feature for security reasons + encryption_rsa_key = "base64 encoded rsa key" + # This will enable the api to use the tls certificate + enable_tls = true + # This will enable the tls port + tls_port = "8443" + # This will enable the tls certificate + tls_certificate = "base64 encoded tls cert" + # This will enable the tls private key + tls_private_key = "base64 encoded tls key" + # This will enable the catalog caching, this will cache the catalog in the host + disable_catalog_caching = false + # This will enable the orchestrator resources, this will enable the host to use the orchestrator use_orchestrator_resources = false + # This will enable the port forwarding reverse proxy in the host, you will need to set the + # port_forwarding block to configure the ports in the deploy or any other provider + enable_port_forwarding = false + # This will allow more fine tune of the api configuration, you can pass any compatible environment + # variable environment_variables = { "key" = "value" } } + # This will contain the configuration for the port forwarding reverse proxy + # in this case we are opening a port to any part in the host, it will not be linked to any + # specific vm or container. by default it will listen on 0.0.0.0 (all interfaces) + # and the target host will also be 0.0.0.0 (all interfaces) so it will be open to the world + # use + reverse_proxy_host { + port = "2022" + + tcp_route { + target_port = "22" + } + } + # This will contain the configuration for the Parallels Desktop Orchestrator # and how to register this instance with it orchestrator_registration { diff --git a/examples/resources/parallels-desktop_remote_vm/resource.tf b/examples/resources/parallels-desktop_remote_vm/resource.tf index ed41576..0ec0bc9 100644 --- a/examples/resources/parallels-desktop_remote_vm/resource.tf +++ b/examples/resources/parallels-desktop_remote_vm/resource.tf @@ -1,11 +1,23 @@ resource "parallels-desktop_remote_vm" "example_box" { - host = "https://example.com:8080" - name = "example-vm" - owner = "example" - catalog_id = "example-catalog-id" - version = "v1" - host_connection = "host=user:VerySecretPassword@example.com" - path = "/Users/example/Parallels" + # You can only use one of the following options + + # Use the host if you need to connect directly to a host + host = "http://example.com:8080" + # Use the orchestrator if you need to connect to a Parallels Orchestrator + orchestrator = "https://orchestrator.example.com:443" + + # The name of the VM + name = "example-vm" + # The owner of the VM, otherwise it will be set as root + owner = "example" + # The catalog id of the VM from the catalog provider + catalog_id = "example-catalog-id" + # The version of the VM from the catalog provider + version = "v1" + # The connection to the catalog provider + catalog_connection = "host=user:VerySecretPassword@example.com" + # The path where the VM will be stored + path = "/Users/example/Parallels" # This will tell how should we authenticate with the host API # you can either use it or leave it empty, if left empty then @@ -29,6 +41,24 @@ resource "parallels-desktop_remote_vm" "example_box" { memory_size = "2048" } + # this flag will set the desired state for the VM + # if it is set to true it will keep the VM running otherwise it will stop it + # by default it is set to true, so all VMs will be running + keep_running = true + + # This will contain the configuration for the port forwarding reverse proxy + # in this case we are opening a port to any part in the host, it will not be linked to any + # specific vm or container. by default it will listen on 0.0.0.0 (all interfaces) + # and the target host will also be 0.0.0.0 (all interfaces) so it will be open to the world + # use + reverse_proxy_host { + port = "2022" + + tcp_route { + target_port = "22" + } + } + # this will allow you to fine grain the configuration of the VM # you can pass any command that is compatible with the prlctl command # directly to the VM @@ -85,4 +115,4 @@ resource "parallels-desktop_remote_vm" "example_box" { "rm -rf /tmp/*" ] } -} \ No newline at end of file +} diff --git a/examples/resources/parallels-desktop_vagrant_box/resource.tf b/examples/resources/parallels-desktop_vagrant_box/resource.tf index 45c94a5..9275e7c 100644 --- a/examples/resources/parallels-desktop_vagrant_box/resource.tf +++ b/examples/resources/parallels-desktop_vagrant_box/resource.tf @@ -34,6 +34,24 @@ resource "parallels-desktop_remote_vm" "example_vagrant_file" { memory_size = "2048" } + # this flag will set the desired state for the VM + # if it is set to true it will keep the VM running otherwise it will stop it + # by default it is set to true, so all VMs will be running + keep_running = true + + # This will contain the configuration for the port forwarding reverse proxy + # in this case we are opening a port to any part in the host, it will not be linked to any + # specific vm or container. by default it will listen on 0.0.0.0 (all interfaces) + # and the target host will also be 0.0.0.0 (all interfaces) so it will be open to the world + # use + reverse_proxy_host { + port = "2022" + + tcp_route { + target_port = "22" + } + } + # this will allow you to fine grain the configuration of the VM # you can pass any command that is compatible with the prlctl command # directly to the VM @@ -91,4 +109,4 @@ resource "parallels-desktop_remote_vm" "example_vagrant_file" { ] } -} \ No newline at end of file +} diff --git a/internal/apiclient/apimodels/create_reverse_proxy_host.go b/internal/apiclient/apimodels/create_reverse_proxy_host.go new file mode 100644 index 0000000..f2fd112 --- /dev/null +++ b/internal/apiclient/apimodels/create_reverse_proxy_host.go @@ -0,0 +1,17 @@ +package apimodels + +type ReverseProxyHostCreateRequest struct { + Host string `json:"host"` + Port string `json:"port"` + Tls *ReverseProxyHostTls `json:"tls,omitempty"` + Cors *ReverseProxyHostCors `json:"cors,omitempty"` + HttpRoutes []*ReverseProxyHostHttpRoute `json:"http_routes,omitempty"` + TcpRoute *ReverseProxyHostTcpRoute `json:"tcp_route,omitempty"` +} + +type ReverseProxyHostUpdateRequest struct { + Host string `json:"host"` + Port string `json:"port"` + Tls *ReverseProxyHostTls `json:"tls,omitempty"` + Cors *ReverseProxyHostCors `json:"cors,omitempty"` +} diff --git a/internal/apiclient/apimodels/orchestrator_host.go b/internal/apiclient/apimodels/orchestrator_host.go index c9a3b05..de25c0e 100644 --- a/internal/apiclient/apimodels/orchestrator_host.go +++ b/internal/apiclient/apimodels/orchestrator_host.go @@ -26,19 +26,39 @@ type OrchestratorHostResponse struct { } type OrchestratorHost struct { - ID string `json:"id"` - Enabled bool `json:"enabled"` - Host string `json:"host"` - Architecture string `json:"architecture"` - CPUModel string `json:"cpu_model"` - Description string `json:"description"` - Tags []string `json:"tags"` - State string `json:"state"` - Resources OrchestratorHostResources `json:"resources"` + ID string `json:"id"` + Enabled bool `json:"enabled"` + Host string `json:"host"` + Architecture string `json:"architecture"` + CpuModel string `json:"cpu_model"` + OsVersion string `json:"os_version,omitempty"` + OsName string `json:"os_name,omitempty"` + ExternalIpAddress string `json:"external_ip_address,omitempty"` + DevOpsVersion string `json:"devops_version,omitempty"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + State string `json:"state,omitempty"` + ParallelsDesktopVersion string `json:"parallels_desktop_version,omitempty"` + ParallelsDesktopLicensed bool `json:"parallels_desktop_licensed,omitempty"` + IsReverseProxyEnabled bool `json:"is_reverse_proxy_enabled"` + ReverseProxy *HostReverseProxy `json:"reverse_proxy,omitempty"` + Resources OrchestratorHostResources `json:"resources"` + RequiredClaims []string `json:"required_claims,omitempty"` + RequiredRoles []string `json:"required_roles,omitempty"` } type OrchestratorHostResources struct { - LogicalCPUCount int64 `json:"logical_cpu_count"` - MemorySize int64 `json:"memory_size"` - DiskSize int64 `json:"disk_size"` + TotalAppleVms int64 `json:"total_apple_vms,omitempty"` + PhysicalCpuCount int64 `json:"physical_cpu_count,omitempty"` + LogicalCpuCount int64 `json:"logical_cpu_count"` + MemorySize float64 `json:"memory_size,omitempty"` + DiskSize float64 `json:"disk_size,omitempty"` + FreeDiskSize float64 `json:"free_disk_size,omitempty"` +} + +type HostReverseProxy struct { + Enabled bool `json:"enabled,omitempty"` + Host string `json:"host,omitempty"` + Port string `json:"port,omitempty"` + Hosts []ReverseProxyHost `json:"hosts,omitempty"` } diff --git a/internal/apiclient/apimodels/reverse_proxy.go b/internal/apiclient/apimodels/reverse_proxy.go new file mode 100644 index 0000000..a291d5b --- /dev/null +++ b/internal/apiclient/apimodels/reverse_proxy.go @@ -0,0 +1,43 @@ +package apimodels + +type ReverseProxyHost struct { + ID string `json:"id"` + Host string `json:"host"` + Port string `json:"port"` + Tls *ReverseProxyHostTls `json:"tls,omitempty"` + Cors *ReverseProxyHostCors `json:"cors,omitempty"` + HttpRoutes []*ReverseProxyHostHttpRoute `json:"http_routes,omitempty"` + TcpRoute *ReverseProxyHostTcpRoute `json:"tcp_route,omitempty"` +} + +type ReverseProxyHostTls struct { + Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + Cert string `json:"cert,omitempty" yaml:"cert,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` +} + +type ReverseProxyHostTcpRoute struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + TargetPort string `json:"target_port,omitempty" yaml:"target_port,omitempty"` + TargetHost string `json:"target_host,omitempty" yaml:"target_host,omitempty"` + TargetVmId string `json:"target_vm_id,omitempty" yaml:"target_vm_id,omitempty"` +} + +type ReverseProxyHostCors struct { + Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + AllowedOrigins []string `json:"allowed_origins,omitempty" yaml:"allowed_origins,omitempty"` + AllowedMethods []string `json:"allowed_methods,omitempty" yaml:"allowed_methods,omitempty"` + AllowedHeaders []string `json:"allowed_headers,omitempty" yaml:"allowed_headers,omitempty"` +} + +type ReverseProxyHostHttpRoute struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + TargetVmId string `json:"target_vm_id,omitempty" yaml:"target_vm_id,omitempty"` + TargetHost string `json:"target_host,omitempty" yaml:"target_host,omitempty"` + TargetPort string `json:"target_port,omitempty" yaml:"target_port,omitempty"` + Schema string `json:"schema,omitempty" yaml:"scheme,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + RequestHeaders map[string]string `json:"request_headers,omitempty" yaml:"request_headers,omitempty"` + ResponseHeaders map[string]string `json:"response_headers,omitempty" yaml:"response_headers,omitempty"` +} diff --git a/internal/apiclient/apimodels/system_usage.go b/internal/apiclient/apimodels/system_usage.go index f33d1ea..78a69e0 100644 --- a/internal/apiclient/apimodels/system_usage.go +++ b/internal/apiclient/apimodels/system_usage.go @@ -1,15 +1,43 @@ package apimodels type SystemUsageResponse struct { - CpuType string `json:"cpu_type,omitempty"` - TotalAvailable SystemUsageItem `json:"total_available,omitempty"` - TotalInUse SystemUsageItem `json:"total_in_use,omitempty"` - TotalReserved SystemUsageItem `json:"total_reserved,omitempty"` + CpuType string `json:"cpu_type,omitempty"` + CpuBrand string `json:"cpu_brand,omitempty"` + DevOpsVersion string `json:"devops_version,omitempty"` + OsName string `json:"os_name,omitempty"` + OsVersion string `json:"os_version,omitempty"` + ParallelsDesktopVersion string `json:"parallels_desktop_version,omitempty"` + ParallelsDesktopLicensed bool `json:"parallels_desktop_licensed,omitempty"` + ExternalIpAddress string `json:"external_ip_address,omitempty"` + IsReverseProxyEnabled bool `json:"is_reverse_proxy_enabled"` + ReverseProxy *SystemReverseProxy `json:"reverse_proxy,omitempty"` + SystemReserved *SystemUsageItem `json:"system_reserved,omitempty"` + Total *SystemUsageItem `json:"total,omitempty"` + TotalAvailable *SystemUsageItem `json:"total_available,omitempty"` + TotalInUse *SystemUsageItem `json:"total_in_use,omitempty"` + TotalReserved *SystemUsageItem `json:"total_reserved,omitempty"` } type SystemUsageItem struct { PhysicalCpuCount int64 `json:"physical_cpu_count,omitempty"` - LogicalCpuCount int64 `json:"logical_cpu_count,omitempty"` + LogicalCpuCount int64 `json:"logical_cpu_count"` + MemorySize float64 `json:"memory_size"` + DiskSize float64 `json:"disk_count"` +} + +type SystemHardwareInfo struct { + CpuType string `json:"cpu_type,omitempty"` + CpuBrand string `json:"cpu_brand,omitempty"` + PhysicalCpuCount int `json:"physical_cpu_count,omitempty"` + LogicalCpuCount int `json:"logical_cpu_count,omitempty"` MemorySize float64 `json:"memory_size,omitempty"` - DiskSize float64 `json:"disk_count,omitempty"` + DiskSize float64 `json:"disk_size,omitempty"` + FreeDiskSize float64 `json:"free_disk_size,omitempty"` +} + +type SystemReverseProxy struct { + Enabled bool `json:"enabled,omitempty"` + Host string `json:"host,omitempty"` + Port string `json:"port,omitempty"` + Hosts []ReverseProxyHost `json:"hosts,omitempty"` } diff --git a/internal/apiclient/apimodels/virtual_machine.go b/internal/apiclient/apimodels/virtual_machine.go index 6484fdc..59566d7 100644 --- a/internal/apiclient/apimodels/virtual_machine.go +++ b/internal/apiclient/apimodels/virtual_machine.go @@ -3,6 +3,9 @@ package apimodels type VirtualMachine struct { User string `json:"user"` ID string `json:"ID"` + HostId string `json:"host_id"` + HostExternalIpAddress string `json:"host_external_ip_address"` + InternalIpAddress string `json:"internal_ip_address"` Name string `json:"Name"` Description string `json:"Description"` Type string `json:"Type"` diff --git a/internal/apiclient/create_reverse_proxy_host.go b/internal/apiclient/create_reverse_proxy_host.go new file mode 100644 index 0000000..5dc3269 --- /dev/null +++ b/internal/apiclient/create_reverse_proxy_host.go @@ -0,0 +1,45 @@ +package apiclient + +import ( + "context" + "fmt" + + "terraform-provider-parallels-desktop/internal/apiclient/apimodels" + "terraform-provider-parallels-desktop/internal/helpers" + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func CreateReverseProxyHost(ctx context.Context, config HostConfig, request apimodels.ReverseProxyHost) (*apimodels.ReverseProxyHost, diag.Diagnostics) { + diagnostics := diag.Diagnostics{} + var response apimodels.ReverseProxyHost + + tflog.Info(ctx, "Creating reverse proxy host "+request.Host+" with port "+request.Port) + urlHost := helpers.GetHostUrl(config.Host) + var url string + if config.IsOrchestrator { + url = fmt.Sprintf("%s/orchestrator/hosts/%s/reverse-proxy/hosts", helpers.GetHostApiVersionedBaseUrl(urlHost), config.HostId) + } else { + url = fmt.Sprintf("%s/reverse-proxy/hosts", helpers.GetHostApiVersionedBaseUrl(urlHost)) + } + + auth, err := authenticator.GetAuthenticator(ctx, urlHost, config.License, config.Authorization, config.DisableTlsValidation) + if err != nil { + diagnostics.AddError("There was an error getting the authenticator", err.Error()) + return nil, diagnostics + } + + client := helpers.NewHttpCaller(ctx, config.DisableTlsValidation) + if clientResponse, err := client.PostDataToClient(url, nil, request, auth, &response); err != nil { + if clientResponse != nil && clientResponse.ApiError != nil { + tflog.Error(ctx, fmt.Sprintf("Error creating reverse proxy: %v, api message: %s", err, clientResponse.ApiError.Message)) + } + + diagnostics.AddError("There was an error creating the reverse proxy", err.Error()) + return nil, diagnostics + } + + return &response, diagnostics +} diff --git a/internal/apiclient/delete_reverse_proxy_host.go b/internal/apiclient/delete_reverse_proxy_host.go new file mode 100644 index 0000000..cdb5e1e --- /dev/null +++ b/internal/apiclient/delete_reverse_proxy_host.go @@ -0,0 +1,44 @@ +package apiclient + +import ( + "context" + "fmt" + + "terraform-provider-parallels-desktop/internal/helpers" + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func DeleteReverseProxyHost(ctx context.Context, config HostConfig, host string) diag.Diagnostics { + diagnostic := diag.Diagnostics{} + urlHost := helpers.GetHostUrl(config.Host) + if host == "" { + diagnostic.AddError("There was an error deleting the reverse proxy host", "host is empty") + return diagnostic + } + + var url string + if config.IsOrchestrator { + url = fmt.Sprintf("%s/orchestrator/hosts/%s/reverse-proxy/hosts/%s", helpers.GetHostApiVersionedBaseUrl(urlHost), config.HostId, host) + } else { + url = fmt.Sprintf("%s/reverse-proxy/hosts/%s", helpers.GetHostApiVersionedBaseUrl(urlHost), host) + } + + auth, err := authenticator.GetAuthenticator(ctx, urlHost, config.License, config.Authorization, config.DisableTlsValidation) + if err != nil { + diagnostic.AddError("There was an error getting the authenticator", err.Error()) + return diagnostic + } + + client := helpers.NewHttpCaller(ctx, config.DisableTlsValidation) + if _, err := client.DeleteDataFromClient(url, nil, auth, nil); err != nil { + diagnostic.AddError("There was an error deleting the reverse proxy host", err.Error()) + return diagnostic + } + + tflog.Info(ctx, "Deleted reverse proxy host "+host) + + return diagnostic +} diff --git a/internal/apiclient/get_orchestrator_host.go b/internal/apiclient/get_orchestrator_host.go index a89aa5b..a7754aa 100644 --- a/internal/apiclient/get_orchestrator_host.go +++ b/internal/apiclient/get_orchestrator_host.go @@ -45,3 +45,33 @@ func GetOrchestratorHost(ctx context.Context, config HostConfig, hostId string) return &response, diagnostics } + +func GetOrchestratorHosts(ctx context.Context, config HostConfig) ([]apimodels.OrchestratorHost, diag.Diagnostics) { + diagnostics := diag.Diagnostics{} + var response []apimodels.OrchestratorHost + urlHost := helpers.GetHostUrl(config.Host) + + url := fmt.Sprintf("%s/orchestrator/hosts", helpers.GetHostApiVersionedBaseUrl(urlHost)) + + auth, err := authenticator.GetAuthenticator(ctx, urlHost, config.License, config.Authorization, config.DisableTlsValidation) + if err != nil { + diagnostics.AddError("There was an error getting the authenticator", err.Error()) + return nil, diagnostics + } + + client := helpers.NewHttpCaller(ctx, config.DisableTlsValidation) + if clientResponse, err := client.GetDataFromClient(url, nil, auth, &response); err != nil { + if clientResponse != nil && clientResponse.ApiError != nil { + if clientResponse.ApiError.Code == 404 { + return nil, diagnostics + } + tflog.Error(ctx, fmt.Sprintf("Error getting orchestrator hosts: %v, api message: %s", err, clientResponse.ApiError.Message)) + } + diagnostics.AddError("There was an error getting the orchestrator hosts", err.Error()) + return nil, diagnostics + } + + tflog.Info(ctx, fmt.Sprintf("Got %v orchestrators ", len(response))) + + return response, diagnostics +} diff --git a/internal/apiclient/get_reverse_proxy_host.go b/internal/apiclient/get_reverse_proxy_host.go new file mode 100644 index 0000000..11f44ec --- /dev/null +++ b/internal/apiclient/get_reverse_proxy_host.go @@ -0,0 +1,52 @@ +package apiclient + +import ( + "context" + "fmt" + + "terraform-provider-parallels-desktop/internal/apiclient/apimodels" + "terraform-provider-parallels-desktop/internal/helpers" + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func GetReverseProxyHost(ctx context.Context, config HostConfig, host string) (*apimodels.ReverseProxyHost, diag.Diagnostics) { + diagnostic := diag.Diagnostics{} + urlHost := helpers.GetHostUrl(config.Host) + if host == "" { + diagnostic.AddError("There was an error getting the reverse proxy host", "host is empty") + return nil, diagnostic + } + + var url string + if config.IsOrchestrator { + url = fmt.Sprintf("%s/orchestrator/hosts/%s/reverse-proxy/hosts/%s", helpers.GetHostApiVersionedBaseUrl(urlHost), config.HostId, host) + } else { + url = fmt.Sprintf("%s/reverse-proxy/hosts/%s", helpers.GetHostApiVersionedBaseUrl(urlHost), host) + } + + auth, err := authenticator.GetAuthenticator(ctx, urlHost, config.License, config.Authorization, config.DisableTlsValidation) + if err != nil { + diagnostic.AddError("There was an error getting the authenticator", err.Error()) + return nil, diagnostic + } + + var response apimodels.ReverseProxyHost + client := helpers.NewHttpCaller(ctx, config.DisableTlsValidation) + if clientResponse, err := client.GetDataFromClient(url, nil, auth, &response); err != nil { + if clientResponse != nil && clientResponse.ApiError != nil { + if clientResponse.ApiError.Code == 404 { + return nil, diagnostic + } + tflog.Error(ctx, fmt.Sprintf("Error getting claims: %v, api message: %s", err, clientResponse.ApiError.Message)) + } + diagnostic.AddError("There was an error getting the claims", err.Error()) + return nil, diagnostic + } + + tflog.Info(ctx, "Got the reverse proxy host "+host) + + return &response, diagnostic +} diff --git a/internal/apiclient/get_system_usage.go b/internal/apiclient/get_system_usage.go index 70b3dc4..f4b7638 100644 --- a/internal/apiclient/get_system_usage.go +++ b/internal/apiclient/get_system_usage.go @@ -17,7 +17,12 @@ func GetSystemUsage(ctx context.Context, config HostConfig) (*apimodels.SystemUs var response apimodels.SystemUsageResponse urlHost := helpers.GetHostUrl(config.Host) - url := fmt.Sprintf("%s/config/hardware", helpers.GetHostApiVersionedBaseUrl(urlHost)) + var url string + if config.IsOrchestrator { + url = fmt.Sprintf("%s/orchestrator/hosts/%s/hardware", helpers.GetHostApiVersionedBaseUrl(urlHost), config.HostId) + } else { + url = fmt.Sprintf("%s/config/hardware", helpers.GetHostApiVersionedBaseUrl(urlHost)) + } auth, err := authenticator.GetAuthenticator(ctx, urlHost, config.License, config.Authorization, config.DisableTlsValidation) if err != nil { diff --git a/internal/apiclient/host_config.go b/internal/apiclient/host_config.go index c410afa..db63239 100644 --- a/internal/apiclient/host_config.go +++ b/internal/apiclient/host_config.go @@ -7,6 +7,7 @@ import ( type HostConfig struct { IsOrchestrator bool `json:"is_orchestrator"` Host string `json:"host"` + HostId string `json:"host_id"` MachineId string `json:"machine_id"` License string `json:"license"` DisableTlsValidation bool `json:"disable_tls_validation"` diff --git a/internal/clone_vm/resource_models.go b/internal/clone_vm/models/resource_models_v0.go similarity index 94% rename from internal/clone_vm/resource_models.go rename to internal/clone_vm/models/resource_models_v0.go index 0527e4f..68fb24a 100644 --- a/internal/clone_vm/resource_models.go +++ b/internal/clone_vm/models/resource_models_v0.go @@ -1,4 +1,4 @@ -package clonevm +package models import ( "terraform-provider-parallels-desktop/internal/schemas/authenticator" @@ -12,8 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -// CloneVmResourceModel describes the resource data model. -type CloneVmResourceModel struct { +// CloneVmResourceModelV0 describes the resource data model. +type CloneVmResourceModelV0 struct { Authenticator *authenticator.Authentication `tfsdk:"authenticator"` Host types.String `tfsdk:"host"` Orchestrator types.String `tfsdk:"orchestrator"` diff --git a/internal/clone_vm/models/resource_models_v1.go b/internal/clone_vm/models/resource_models_v1.go new file mode 100644 index 0000000..dc14e71 --- /dev/null +++ b/internal/clone_vm/models/resource_models_v1.go @@ -0,0 +1,40 @@ +package models + +import ( + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/prlctl" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" + "terraform-provider-parallels-desktop/internal/schemas/sharedfolder" + "terraform-provider-parallels-desktop/internal/schemas/vmconfig" + "terraform-provider-parallels-desktop/internal/schemas/vmspecs" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// CloneVmResourceModelV0 describes the resource data model. +type CloneVmResourceModelV1 struct { + Authenticator *authenticator.Authentication `tfsdk:"authenticator"` + Host types.String `tfsdk:"host"` + Orchestrator types.String `tfsdk:"orchestrator"` + ID types.String `tfsdk:"id"` + OsType types.String `tfsdk:"os_type"` + BaseVmId types.String `tfsdk:"base_vm_id"` + ExternalIp types.String `tfsdk:"external_ip"` + InternalIp types.String `tfsdk:"internal_ip"` + Name types.String `tfsdk:"name"` + Owner types.String `tfsdk:"owner"` + Path types.String `tfsdk:"path"` + Specs *vmspecs.VmSpecs `tfsdk:"specs"` + PostProcessorScripts []*postprocessorscript.PostProcessorScript `tfsdk:"post_processor_script"` + OnDestroyScript []*postprocessorscript.PostProcessorScript `tfsdk:"on_destroy_script"` + SharedFolder []*sharedfolder.SharedFolder `tfsdk:"shared_folder"` + Config *vmconfig.VmConfig `tfsdk:"config"` + PrlCtl []*prlctl.PrlCtlCmd `tfsdk:"prlctl"` + RunAfterCreate types.Bool `tfsdk:"run_after_create"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + ForceChanges types.Bool `tfsdk:"force_changes"` + KeepRunning types.Bool `tfsdk:"keep_running"` + ReverseProxyHosts []*reverseproxy.ReverseProxyHost `tfsdk:"reverse_proxy_host"` +} diff --git a/internal/clone_vm/resource.go b/internal/clone_vm/resource.go index edce3d0..fe3166a 100644 --- a/internal/clone_vm/resource.go +++ b/internal/clone_vm/resource.go @@ -7,12 +7,16 @@ import ( "terraform-provider-parallels-desktop/internal/apiclient" "terraform-provider-parallels-desktop/internal/apiclient/apimodels" + resource_models "terraform-provider-parallels-desktop/internal/clone_vm/models" + "terraform-provider-parallels-desktop/internal/clone_vm/schemas" "terraform-provider-parallels-desktop/internal/common" "terraform-provider-parallels-desktop/internal/models" "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" "terraform-provider-parallels-desktop/internal/telemetry" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" @@ -39,7 +43,7 @@ func (r *CloneVmResource) Metadata(ctx context.Context, req resource.MetadataReq } func (r *CloneVmResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = getSchema(ctx) + resp.Schema = schemas.GetCloneVmSchemaV1(ctx) } func (r *CloneVmResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -60,7 +64,7 @@ func (r *CloneVmResource) Configure(ctx context.Context, req resource.ConfigureR } func (r *CloneVmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data CloneVmResourceModel + var data resource_models.CloneVmResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -252,25 +256,92 @@ func (r *CloneVmResource) Create(ctx context.Context, req resource.CreateRequest return } - // Starting the vm if requested - if data.RunAfterCreate.ValueBool() { + if len(data.ReverseProxyHosts) > 0 { + rpHostConfig := hostConfig + rpHostConfig.HostId = stoppedVm.HostId + rpHosts, updateDiag := updateReverseProxyHostsTarget(ctx, &data, rpHostConfig, stoppedVm) + if updateDiag.HasError() { + resp.Diagnostics.Append(updateDiag...) + return + } + + result, createDiag := reverseproxy.Create(ctx, rpHostConfig, rpHosts) + if createDiag.HasError() { + resp.Diagnostics.Append(createDiag...) + + if diag := reverseproxy.Delete(ctx, rpHostConfig, rpHosts); diag.HasError() { + tflog.Error(ctx, "Error deleting reverse proxy hosts") + } + + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, rpHostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, rpHostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + + apiclient.DeleteVm(ctx, rpHostConfig, data.ID.ValueString()) + } + return + } + + for i := range result { + data.ReverseProxyHosts[i].ID = result[i].ID + } + } + + // Starting the vm by default, otherwise we will stop the VM from being created + if data.RunAfterCreate.ValueBool() || data.KeepRunning.ValueBool() || (data.RunAfterCreate.IsUnknown() && data.KeepRunning.IsUnknown()) { if _, diag := common.EnsureMachineRunning(ctx, hostConfig, stoppedVm); diag.HasError() { resp.Diagnostics.Append(diag...) if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return } - _, diag := apiclient.GetVm(ctx, hostConfig, clonedVm.ID) + _, diag := apiclient.GetVm(ctx, hostConfig, vm.ID) if diag.HasError() { resp.Diagnostics.Append(diag...) return } + } else { + // If we are not starting the machine, we will stop it + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } + } + + externalIp := "" + internalIp := "" + refreshVm, refreshDiag := apiclient.GetVm(ctx, hostConfig, vm.ID) + if refreshDiag.HasError() { + resp.Diagnostics.Append(refreshDiag...) + return + } else { + externalIp = refreshVm.HostExternalIpAddress + internalIp = refreshVm.InternalIpAddress } + data.ExternalIp = types.StringValue(externalIp) + data.InternalIp = types.StringValue(internalIp) data.OsType = types.StringValue(clonedVm.OS) if data.OnDestroyScript != nil { for _, script := range data.OnDestroyScript { @@ -311,7 +382,7 @@ func (r *CloneVmResource) Create(ctx context.Context, req resource.CreateRequest } func (r *CloneVmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data CloneVmResourceModel + var data resource_models.CloneVmResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -383,8 +454,8 @@ func (r *CloneVmResource) Read(ctx context.Context, req resource.ReadRequest, re } func (r *CloneVmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data CloneVmResourceModel - var currentData CloneVmResourceModel + var data resource_models.CloneVmResourceModelV1 + var currentData resource_models.CloneVmResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -532,8 +603,82 @@ func (r *CloneVmResource) Update(ctx context.Context, req resource.UpdateRequest } } - data.ID = types.StringValue(vm.ID) - data.OsType = types.StringValue(vm.OS) + if reverseproxy.ReverseProxyHostsDiff(data.ReverseProxyHosts, currentData.ReverseProxyHosts) { + copyCurrentRpHosts := reverseproxy.CopyReverseProxyHosts(currentData.ReverseProxyHosts) + copyRpHosts := reverseproxy.CopyReverseProxyHosts(data.ReverseProxyHosts) + + results, updateDiag := reverseproxy.Update(ctx, hostConfig, copyCurrentRpHosts, copyRpHosts) + if updateDiag.HasError() { + resp.Diagnostics.Append(updateDiag...) + revertResults, _ := reverseproxy.Revert(ctx, hostConfig, copyCurrentRpHosts, copyRpHosts) + for i := range revertResults { + data.ReverseProxyHosts[i].ID = revertResults[i].ID + } + return + } + + for i := range results { + data.ReverseProxyHosts[i].ID = results[i].ID + } + } else { + for i := range currentData.ReverseProxyHosts { + data.ReverseProxyHosts[i].ID = currentData.ReverseProxyHosts[i].ID + } + } + + // Starting the vm by default, otherwise we will stop the VM from being created + if data.RunAfterCreate.ValueBool() || data.KeepRunning.ValueBool() || (data.RunAfterCreate.IsUnknown() && data.KeepRunning.IsUnknown()) { + if _, diag := common.EnsureMachineRunning(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } + + _, diag := apiclient.GetVm(ctx, hostConfig, vm.ID) + if diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + } else { + // If we are not starting the machine, we will stop it + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } + } + + externalIp := "" + internalIp := "" + refreshVm, refreshDiag := apiclient.GetVm(ctx, hostConfig, vm.ID) + if refreshDiag.HasError() { + resp.Diagnostics.Append(refreshDiag...) + return + } else { + externalIp = refreshVm.HostExternalIpAddress + internalIp = refreshVm.InternalIpAddress + } + + data.ID = types.StringValue(refreshVm.ID) + data.OsType = types.StringValue(refreshVm.OS) + data.ExternalIp = types.StringValue(externalIp) + data.InternalIp = types.StringValue(internalIp) if data.OnDestroyScript != nil { for _, script := range data.OnDestroyScript { elements := make([]attr.Value, 0) @@ -569,7 +714,7 @@ func (r *CloneVmResource) Update(ctx context.Context, req resource.UpdateRequest } func (r *CloneVmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data CloneVmResourceModel + var data resource_models.CloneVmResourceModelV1 // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -661,3 +806,119 @@ func (r *CloneVmResource) Delete(ctx context.Context, req resource.DeleteRequest func (r *CloneVmResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +func (r *CloneVmResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { + v0Schema := schemas.GetCloneVmSchemaV0(ctx) + return map[int64]resource.StateUpgrader{ + 0: { + PriorSchema: &v0Schema, + StateUpgrader: UpgradeStateToV1, + }, + } +} + +func UpgradeStateToV1(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData resource_models.CloneVmResourceModelV0 + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := resource_models.CloneVmResourceModelV1{ + Authenticator: priorStateData.Authenticator, + Host: priorStateData.Host, + Orchestrator: priorStateData.Orchestrator, + ID: priorStateData.ID, + OsType: priorStateData.OsType, + BaseVmId: priorStateData.BaseVmId, + ExternalIp: types.StringUnknown(), + InternalIp: types.StringUnknown(), + Name: priorStateData.Name, + Owner: priorStateData.Owner, + Path: priorStateData.Path, + Specs: priorStateData.Specs, + PostProcessorScripts: priorStateData.PostProcessorScripts, + OnDestroyScript: priorStateData.OnDestroyScript, + SharedFolder: priorStateData.SharedFolder, + Config: priorStateData.Config, + PrlCtl: priorStateData.PrlCtl, + RunAfterCreate: priorStateData.RunAfterCreate, + Timeouts: priorStateData.Timeouts, + ForceChanges: priorStateData.ForceChanges, + KeepRunning: types.BoolValue(true), + ReverseProxyHosts: make([]*reverseproxy.ReverseProxyHost, 0), + } + + println(fmt.Sprintf("Upgrading state from version %v", upgradedStateData)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &upgradedStateData)...) +} + +func updateReverseProxyHostsTarget(ctx context.Context, data *resource_models.CloneVmResourceModelV1, hostConfig apiclient.HostConfig, targetVm *apimodels.VirtualMachine) ([]reverseproxy.ReverseProxyHost, diag.Diagnostics) { + resultDiagnostic := diag.Diagnostics{} + var refreshedVm *apimodels.VirtualMachine + var rpDiag diag.Diagnostics + refreshedVm, rpDiag = common.EnsureMachineHasInternalIp(ctx, hostConfig, targetVm) + if rpDiag.HasError() { + resultDiagnostic.Append(rpDiag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, refreshedVm); diag.HasError() { + return nil, diag + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return nil, resultDiagnostic + } + + modifiedHosts := make([]reverseproxy.ReverseProxyHost, len(data.ReverseProxyHosts)) + for i := range data.ReverseProxyHosts { + host := reverseproxy.ReverseProxyHost{} + host.Host = data.ReverseProxyHosts[i].Host + host.Port = data.ReverseProxyHosts[i].Port + internalIp := refreshedVm.InternalIpAddress + emptyString := "" + + if data.ReverseProxyHosts[i].Cors != nil { + host.Cors = &reverseproxy.ReverseProxyCors{} + host.Cors.AllowedOrigins = data.ReverseProxyHosts[i].Cors.AllowedOrigins + host.Cors.AllowedMethods = data.ReverseProxyHosts[i].Cors.AllowedMethods + host.Cors.AllowedHeaders = data.ReverseProxyHosts[i].Cors.AllowedHeaders + host.Cors.Enabled = data.ReverseProxyHosts[i].Cors.Enabled + } + if data.ReverseProxyHosts[i].Tls != nil { + host.Tls = &reverseproxy.ReverseProxyTls{} + host.Tls.Certificate = data.ReverseProxyHosts[i].Tls.Certificate + host.Tls.PrivateKey = data.ReverseProxyHosts[i].Tls.PrivateKey + host.Tls.Enabled = data.ReverseProxyHosts[i].Tls.Enabled + } + if data.ReverseProxyHosts[i].TcpRoute != nil { + host.TcpRoute = &reverseproxy.ReverseProxyHostTcpRoute{} + host.TcpRoute.TargetPort = data.ReverseProxyHosts[i].TcpRoute.TargetPort + host.TcpRoute.TargetHost = types.StringValue(internalIp) + host.TcpRoute.TargetVmId = types.StringValue(emptyString) + } + + if len(data.ReverseProxyHosts[i].HttpRoute) > 0 { + host.HttpRoute = make([]*reverseproxy.ReverseProxyHttpRoute, len(data.ReverseProxyHosts[i].HttpRoute)) + for j := range modifiedHosts[i].HttpRoute { + httpRoute := reverseproxy.ReverseProxyHttpRoute{} + httpRoute.Path = data.ReverseProxyHosts[i].HttpRoute[j].Path + httpRoute.TargetHost = types.StringValue(internalIp) + httpRoute.TargetPort = data.ReverseProxyHosts[i].HttpRoute[j].TargetPort + httpRoute.TargetVmId = types.StringValue(emptyString) + httpRoute.Pattern = data.ReverseProxyHosts[i].HttpRoute[j].Pattern + httpRoute.Schema = data.ReverseProxyHosts[i].HttpRoute[j].Schema + httpRoute.RequestHeaders = data.ReverseProxyHosts[i].HttpRoute[j].RequestHeaders + httpRoute.ResponseHeaders = data.ReverseProxyHosts[i].HttpRoute[j].ResponseHeaders + host.HttpRoute[j] = &httpRoute + } + } + + modifiedHosts[i] = host + } + + return modifiedHosts, resultDiagnostic +} diff --git a/internal/clone_vm/resource_schema.go b/internal/clone_vm/schemas/resource_schema_v0.go similarity index 98% rename from internal/clone_vm/resource_schema.go rename to internal/clone_vm/schemas/resource_schema_v0.go index e7cd317..b9a7a24 100644 --- a/internal/clone_vm/resource_schema.go +++ b/internal/clone_vm/schemas/resource_schema_v0.go @@ -1,4 +1,4 @@ -package clonevm +package schemas import ( "context" @@ -19,7 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) -func getSchema(ctx context.Context) schema.Schema { +func GetCloneVmSchemaV0(ctx context.Context) schema.Schema { return schema.Schema{ // This description is used by the documentation generator and the language server. MarkdownDescription: "Parallels Desktop Clone VM resource", diff --git a/internal/clone_vm/schemas/resource_schema_v1.go b/internal/clone_vm/schemas/resource_schema_v1.go new file mode 100644 index 0000000..7dd728b --- /dev/null +++ b/internal/clone_vm/schemas/resource_schema_v1.go @@ -0,0 +1,123 @@ +package schemas + +import ( + "context" + + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/prlctl" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" + "terraform-provider-parallels-desktop/internal/schemas/sharedfolder" + "terraform-provider-parallels-desktop/internal/schemas/vmconfig" + "terraform-provider-parallels-desktop/internal/schemas/vmspecs" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func GetCloneVmSchemaV1(ctx context.Context) schema.Schema { + return schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Parallels Desktop Clone VM resource", + Blocks: map[string]schema.Block{ + authenticator.SchemaName: authenticator.SchemaBlock, + vmspecs.SchemaName: vmspecs.SchemaBlock, + postprocessorscript.SchemaName: postprocessorscript.SchemaBlock, + "on_destroy_script": postprocessorscript.SchemaBlock, + sharedfolder.SchemaName: sharedfolder.SchemaBlock, + vmconfig.SchemaName: vmconfig.SchemaBlock, + prlctl.SchemaName: prlctl.SchemaBlock, + reverseproxy.SchemaName: reverseproxy.HostBlockV0, + }, + Attributes: map[string]schema.Attribute{ + "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ + Create: true, + }), + "force_changes": schema.BoolAttribute{ + MarkdownDescription: "Force changes, this will force the VM to be stopped and started again", + Optional: true, + }, + "host": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Host", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + }, + "orchestrator": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Orchestrator", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine Id", + Computed: true, + }, + "os_type": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine OS type", + Computed: true, + }, + "base_vm_id": schema.StringAttribute{ + MarkdownDescription: "Base Virtual Machine Id to clone", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine name to create, this needs to be unique in the host", + Required: true, + }, + "owner": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine owner", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "path": schema.StringAttribute{ + MarkdownDescription: "Path", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "run_after_create": schema.BoolAttribute{ + MarkdownDescription: "Run after create, this will make the VM to run after creation", + Optional: true, + DeprecationMessage: "Use the `keep_running` attribute instead", + }, + "external_ip": schema.StringAttribute{ + MarkdownDescription: "VM external IP address", + Computed: true, + }, + "internal_ip": schema.StringAttribute{ + MarkdownDescription: "VM internal IP address", + Computed: true, + }, + "keep_running": schema.BoolAttribute{ + MarkdownDescription: "This will keep the VM running after the terraform apply", + Computed: true, + }, + }, + } +} diff --git a/internal/common/basetype_helpers.go b/internal/common/basetype_helpers.go new file mode 100644 index 0000000..5f681c1 --- /dev/null +++ b/internal/common/basetype_helpers.go @@ -0,0 +1,37 @@ +package common + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func IsTrue(c types.Bool) bool { + if c.IsNull() { + return false + } + if c.IsUnknown() { + return false + } + if c.ValueBool() { + return true + } + return false +} + +func GetString(c types.String) string { + if c.IsNull() { + return "" + } + if c.IsUnknown() { + return "" + } + return c.ValueString() +} + +func CopyPointer[T any](src *T) *T { + if src == nil { + return nil + } + dst := new(T) + *dst = *src + return dst +} diff --git a/internal/common/check_required_specs.go b/internal/common/check_required_specs.go index e8c981f..a8e3890 100644 --- a/internal/common/check_required_specs.go +++ b/internal/common/check_required_specs.go @@ -74,7 +74,7 @@ func CheckIfEnoughSpecs(ctx context.Context, hostConfig apiclient.HostConfig, sp diagnostics.AddError("error converting cpu count", err.Error()) return diagnostics } - if hardwareInfo.TotalAvailable.LogicalCpuCount-int64(updateCpuValueInt) <= 0 { + if hardwareInfo.TotalAvailable.LogicalCpuCount-int64(updateCpuValueInt) < 0 { diagnostics.AddError("not enough cpus", "not enough cpus") return diagnostics } @@ -88,7 +88,7 @@ func CheckIfEnoughSpecs(ctx context.Context, hostConfig apiclient.HostConfig, sp diagnostics.AddError("error converting memory size", err.Error()) return diagnostics } - if hardwareInfo.TotalAvailable.MemorySize-float64(updateMemoryValueInt) <= 0 { + if hardwareInfo.TotalAvailable.MemorySize-float64(updateMemoryValueInt) < 0 { diagnostics.AddError("not enough memory", "not enough memory") return diagnostics } diff --git a/internal/common/ensure_machine_as_internal_ip.go b/internal/common/ensure_machine_as_internal_ip.go new file mode 100644 index 0000000..9961205 --- /dev/null +++ b/internal/common/ensure_machine_as_internal_ip.go @@ -0,0 +1,55 @@ +package common + +import ( + "context" + "time" + + "terraform-provider-parallels-desktop/internal/apiclient" + "terraform-provider-parallels-desktop/internal/apiclient/apimodels" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func EnsureMachineHasInternalIp(ctx context.Context, hostConfig apiclient.HostConfig, vm *apimodels.VirtualMachine) (*apimodels.VirtualMachine, diag.Diagnostics) { + diagnostics := diag.Diagnostics{} + + refreshVm, ensureRunningDiag := EnsureMachineRunning(ctx, hostConfig, vm) + if ensureRunningDiag.HasError() { + diagnostics.Append(ensureRunningDiag...) + return nil, diagnostics + } + + maxRetries := 10 + retryCount := 0 + for { + diagnostics = diag.Diagnostics{} + retryCount += 1 + if refreshVm.InternalIpAddress == "" || refreshVm.InternalIpAddress == "-" { + updatedVm, checkVmDiag := apiclient.GetVm(ctx, hostConfig, refreshVm.ID) + if checkVmDiag.HasError() { + diagnostics.Append(checkVmDiag...) + } + + // If we have the internal IP, break out of the loop + if updatedVm.InternalIpAddress != "" && updatedVm.InternalIpAddress != "-" { + tflog.Info(ctx, "Machine "+updatedVm.Name+" is running") + diagnostics = diag.Diagnostics{} + refreshVm = updatedVm + break + } + + // We have run out of retries, add an error and break out of the loop + if retryCount >= maxRetries { + diagnostics.AddError("error getting vm Internal IP", "We could not get the internal IP of the machine") + break + } + + time.Sleep(10 * time.Second) + } else { + break + } + } + + return refreshVm, diagnostics +} diff --git a/internal/common/ensure_machine_running.go b/internal/common/ensure_machine_running.go index 8aba238..d5d082f 100644 --- a/internal/common/ensure_machine_running.go +++ b/internal/common/ensure_machine_running.go @@ -44,12 +44,20 @@ func EnsureMachineRunning(ctx context.Context, hostConfig apiclient.HostConfig, diagnostics.Append(checkVmDiag...) } - // All if good, break out of the loop + // The machine is running, lets check if we have the tools initialized if updatedVm.State == "running" { - tflog.Info(ctx, "Machine "+returnVm.Name+" is running") - diagnostics = diag.Diagnostics{} - returnVm = updatedVm - break + echoHelloCommand := apimodels.PostScriptItem{ + Command: "echo 'I am running'", + VirtualMachineId: updatedVm.ID, + } + + // Only breaking out of the loop if the script executes successfully + if _, execDiag := apiclient.ExecuteScript(ctx, hostConfig, echoHelloCommand); !execDiag.HasError() { + tflog.Info(ctx, "Machine "+returnVm.Name+" is running") + diagnostics = diag.Diagnostics{} + returnVm = updatedVm + break + } } // We have run out of retries, add an error and break out of the loop diff --git a/internal/common/ensure_machine_stopped.go b/internal/common/ensure_machine_stopped.go index 24f0106..3e04c59 100644 --- a/internal/common/ensure_machine_stopped.go +++ b/internal/common/ensure_machine_stopped.go @@ -21,7 +21,7 @@ func EnsureMachineStopped(ctx context.Context, hostConfig apiclient.HostConfig, return vm, diagnostics } - maxRetries := 10 + maxRetries := 30 retryCount := 0 for { diagnostics = diag.Diagnostics{} diff --git a/internal/common/specs.go b/internal/common/specs.go index 1f438e0..0964823 100644 --- a/internal/common/specs.go +++ b/internal/common/specs.go @@ -8,7 +8,6 @@ import ( "terraform-provider-parallels-desktop/internal/apiclient" "terraform-provider-parallels-desktop/internal/apiclient/apimodels" - "terraform-provider-parallels-desktop/internal/helpers" "terraform-provider-parallels-desktop/internal/schemas/vmspecs" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -46,15 +45,8 @@ func SpecsBlockOnUpdate(ctx context.Context, hostConfig apiclient.HostConfig, vm changes := apimodels.NewVmConfigRequest(vm.User) if vm.State == "running" { - // Because this is an update we need to take into account the already existing cpu and add it to the total available - // if the vm is running, otherwise we already added that value to the reserved resources - hardwareInfo.TotalAvailable.LogicalCpuCount = hardwareInfo.TotalAvailable.LogicalCpuCount + vm.Hardware.CPU.Cpus - currentMemoryUsage, err := helpers.GetSizeByteFromString(vm.Hardware.Memory.Size) - if err != nil { - diagnostics.AddError("error getting memory size", err.Error()) - return diagnostics - } - hardwareInfo.TotalAvailable.MemorySize = hardwareInfo.TotalAvailable.MemorySize + helpers.ConvertByteToMegabyte(currentMemoryUsage) + diagnostics.AddError("cannot update vm", "vm is running") + return diagnostics } if planSpecs.CpuCount.ValueString() != fmt.Sprintf("%v", vm.Hardware.CPU.Cpus) { diff --git a/internal/deploy/api_config_schema.go b/internal/deploy/api_config_schema.go deleted file mode 100644 index 6b7b82c..0000000 --- a/internal/deploy/api_config_schema.go +++ /dev/null @@ -1,232 +0,0 @@ -package deploy - -import ( - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -var ( - ApiConfigSchemaName = "api_config" - apiConfigSchemaBlockV0 = schema.SingleNestedBlock{ - MarkdownDescription: "Parallels Desktop DevOps configuration", - Description: "Parallels Desktop DevOps configuration", - Attributes: map[string]schema.Attribute{ - "port": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps port", - Description: "Parallels Desktop DevOps port", - Optional: true, - }, - "prefix": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps port", - Description: "Parallels Desktop DevOps port", - Optional: true, - }, - "devops_version": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps version to install, if empty the latest will be installed", - Description: "Parallels Desktop DevOps version to install, if empty the latest will be installed", - Optional: true, - }, - "root_password": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps root password", - Description: "Parallels Desktop DevOps root password", - Optional: true, - Sensitive: true, - }, - "hmac_secret": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", - Description: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", - Optional: true, - Sensitive: true, - }, - "encryption_rsa_key": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", - Description: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", - Optional: true, - Sensitive: true, - }, - "log_level": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", - Description: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", - Optional: true, - Validators: []validator.String{ - stringvalidator.OneOf("debug", "info", "warn", "error"), - }, - }, - "enable_tls": schema.BoolAttribute{ - MarkdownDescription: "Parallels Desktop DevOps enable TLS", - Description: "Parallels Desktop DevOps enable TLS", - Optional: true, - }, - "tls_port": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps TLS port", - Description: "Parallels Desktop DevOps TLS port", - Optional: true, - }, - "tls_certificate": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", - Description: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", - Optional: true, - Sensitive: true, - }, - "tls_private_key": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", - Description: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", - Optional: true, - Sensitive: true, - }, - "disable_catalog_caching": schema.BoolAttribute{ - MarkdownDescription: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", - Description: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", - Optional: true, - }, - "token_duration_minutes": schema.StringAttribute{ - MarkdownDescription: "JWT Token duration in minutes", - Description: "JWT Token duration in minutes", - Optional: true, - }, - "mode": schema.StringAttribute{ - MarkdownDescription: "API Operation mode, either orchestrator or catalog", - Optional: true, - Sensitive: true, - Validators: []validator.String{ - stringvalidator.OneOf("orchestrator", "catalog", "api"), - }, - }, - "use_orchestrator_resources": schema.BoolAttribute{ - MarkdownDescription: "Use orchestrator resources", - Optional: true, - }, - "system_reserved_memory": schema.StringAttribute{ - MarkdownDescription: "System reserved memory in MB", - Optional: true, - }, - "system_reserved_cpu": schema.StringAttribute{ - MarkdownDescription: "System reserved CPU in %", - Optional: true, - }, - "system_reserved_disk": schema.StringAttribute{ - MarkdownDescription: "System reserved disk in MB", - Optional: true, - }, - "enable_logging": schema.BoolAttribute{ - MarkdownDescription: "Enable logging", - Optional: true, - }, - }, - } - - apiConfigSchemaBlockV1 = schema.SingleNestedBlock{ - MarkdownDescription: "Parallels Desktop DevOps configuration", - Description: "Parallels Desktop DevOps configuration", - Attributes: map[string]schema.Attribute{ - "port": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps port", - Description: "Parallels Desktop DevOps port", - Optional: true, - }, - "prefix": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps port", - Description: "Parallels Desktop DevOps port", - Optional: true, - }, - "devops_version": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps version to install, if empty the latest will be installed", - Description: "Parallels Desktop DevOps version to install, if empty the latest will be installed", - Optional: true, - }, - "root_password": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps root password", - Description: "Parallels Desktop DevOps root password", - Optional: true, - Sensitive: true, - }, - "hmac_secret": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", - Description: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", - Optional: true, - Sensitive: true, - }, - "encryption_rsa_key": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", - Description: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", - Optional: true, - Sensitive: true, - }, - "log_level": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", - Description: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", - Optional: true, - Validators: []validator.String{ - stringvalidator.OneOf("debug", "info", "warn", "error"), - }, - }, - "enable_tls": schema.BoolAttribute{ - MarkdownDescription: "Parallels Desktop DevOps enable TLS", - Description: "Parallels Desktop DevOps enable TLS", - Optional: true, - }, - "tls_port": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps TLS port", - Description: "Parallels Desktop DevOps TLS port", - Optional: true, - }, - "tls_certificate": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", - Description: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", - Optional: true, - Sensitive: true, - }, - "tls_private_key": schema.StringAttribute{ - MarkdownDescription: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", - Description: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", - Optional: true, - Sensitive: true, - }, - "disable_catalog_caching": schema.BoolAttribute{ - MarkdownDescription: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", - Description: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", - Optional: true, - }, - "token_duration_minutes": schema.StringAttribute{ - MarkdownDescription: "JWT Token duration in minutes", - Description: "JWT Token duration in minutes", - Optional: true, - }, - "mode": schema.StringAttribute{ - MarkdownDescription: "API Operation mode, either orchestrator or catalog", - Optional: true, - Sensitive: true, - Validators: []validator.String{ - stringvalidator.OneOf("orchestrator", "catalog", "api"), - }, - }, - "use_orchestrator_resources": schema.BoolAttribute{ - MarkdownDescription: "Use orchestrator resources", - Optional: true, - }, - "system_reserved_memory": schema.StringAttribute{ - MarkdownDescription: "System reserved memory in MB", - Optional: true, - }, - "system_reserved_cpu": schema.StringAttribute{ - MarkdownDescription: "System reserved CPU in %", - Optional: true, - }, - "system_reserved_disk": schema.StringAttribute{ - MarkdownDescription: "System reserved disk in MB", - Optional: true, - }, - "enable_logging": schema.BoolAttribute{ - MarkdownDescription: "Enable logging", - Optional: true, - }, - "environment_variables": schema.MapAttribute{ - MarkdownDescription: "Environment variables that can be used in the DevOps service, please see documentation to see which variables are available", - Optional: true, - ElementType: types.StringType, - }, - }, - } -) diff --git a/internal/deploy/devops_service.go b/internal/deploy/devops_service.go index 293e324..7f71f59 100644 --- a/internal/deploy/devops_service.go +++ b/internal/deploy/devops_service.go @@ -9,6 +9,7 @@ import ( "strings" "terraform-provider-parallels-desktop/internal/clientmodels" + "terraform-provider-parallels-desktop/internal/deploy/models" "terraform-provider-parallels-desktop/internal/interfaces" "terraform-provider-parallels-desktop/internal/localclient" @@ -350,7 +351,7 @@ func (c *DevOpsServiceClient) UninstallParallelsDesktop() error { return nil } -func (c *DevOpsServiceClient) GetLicense() (*ParallelsDesktopLicense, error) { +func (c *DevOpsServiceClient) GetLicense() (*models.ParallelsDesktopLicense, error) { cmd := c.findPath("prlsrvctl") arguments := []string{"info", "--json"} output, err := c.client.RunCommand(cmd, arguments) @@ -368,7 +369,7 @@ func (c *DevOpsServiceClient) GetLicense() (*ParallelsDesktopLicense, error) { return nil, err } - parallelsLicense := ParallelsDesktopLicense{} + parallelsLicense := models.ParallelsDesktopLicense{} parallelsLicense.FromClientModel(parallelsInfo.License) return ¶llelsLicense, nil } @@ -447,16 +448,19 @@ func (c *DevOpsServiceClient) CompareLicenses(license string) (bool, error) { return false, nil } -func (c *DevOpsServiceClient) InstallDevOpsService(license string, config ParallelsDesktopDevopsConfigV1) (string, error) { +func (c *DevOpsServiceClient) InstallDevOpsService(license string, config models.ParallelsDesktopDevopsConfigV2) (string, error) { // Installing DevOps Service devopsPath := c.findPath("prldevops") if devopsPath == "" { cmd := "/bin/bash" arguments := []string{"-c", "\"$(curl -fsSL https://raw.githubusercontent.com/Parallels/prl-devops-service/main/scripts/install.sh)\"", "-", "--no-service"} - if config.DevOpsVersion.ValueString() != "" && config.DevOpsVersion.ValueString() != "latest" { + if config.DevOpsVersion.ValueString() != "" && config.DevOpsVersion.ValueString() != "latest" && !config.UseLatestBeta.ValueBool() { arguments = append(arguments, "--version", config.DevOpsVersion.ValueString()) } + if config.UseLatestBeta.ValueBool() { + arguments = append(arguments, "--pre-release") + } _, err := c.client.RunCommand(cmd, arguments) if err != nil { return "", errors.New("Error running devops install command, error: " + err.Error()) @@ -483,6 +487,11 @@ func (c *DevOpsServiceClient) InstallDevOpsService(license string, config Parall configFile.EnvironmentVariables[key] = envVar.ValueString() } + // Setting the environment variables for the prldevops service port forwarding + if config.EnablePortForwarding.ValueBool() { + configFile.EnvironmentVariables["ENABLE_REVERSE_PROXY"] = "true" + } + yamlConfig, err := yaml.Marshal(configFile) if err != nil { return "", err @@ -634,7 +643,7 @@ func (c *DevOpsServiceClient) GenerateDefaultRootPassword() (string, error) { return encoded, nil } -func (c *DevOpsServiceClient) generateConfigFile(config ParallelsDesktopDevopsConfigV1) (string, error) { +func (c *DevOpsServiceClient) generateConfigFile(config models.ParallelsDesktopDevopsConfigV2) (string, error) { configPath := "/tmp/service_config.json" configMap := make(map[string]interface{}) if config.Port.ValueString() != "" { diff --git a/internal/deploy/models/common.go b/internal/deploy/models/common.go new file mode 100644 index 0000000..9a32f59 --- /dev/null +++ b/internal/deploy/models/common.go @@ -0,0 +1,178 @@ +package models + +import ( + "context" + + "terraform-provider-parallels-desktop/internal/clientmodels" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +type DeployResourceSshConnection struct { + Host types.String `tfsdk:"host"` + HostPort types.String `tfsdk:"host_port"` + User types.String `tfsdk:"user"` + Password types.String `tfsdk:"password"` + PrivateKey types.String `tfsdk:"private_key"` +} + +type ParallelsDesktopLicense struct { + State types.String `tfsdk:"state"` + Key types.String `tfsdk:"key"` + Restricted types.Bool `tfsdk:"restricted"` +} + +func (p *ParallelsDesktopLicense) FromClientModel(value clientmodels.ParallelsServerLicense) { + p.State = types.StringValue(value.State) + p.Key = types.StringValue(value.Key) + p.Restricted = types.BoolValue(value.Restricted == "true") +} + +func (p *ParallelsDesktopLicense) MapObject() basetypes.ObjectValue { + attributeTypes := make(map[string]attr.Type) + attributeTypes["state"] = types.StringType + attributeTypes["key"] = types.StringType + attributeTypes["restricted"] = types.BoolType + + attrs := map[string]attr.Value{} + attrs["state"] = p.State + attrs["key"] = p.Key + attrs["restricted"] = p.Restricted + + return types.ObjectValueMust(attributeTypes, attrs) +} + +type ParallelsDesktopDevOps struct { + Version types.String `tfsdk:"version"` + Protocol types.String `tfsdk:"protocol"` + Host types.String `tfsdk:"host"` + Port types.String `tfsdk:"port"` + User types.String `tfsdk:"user"` + Password types.String `tfsdk:"password"` +} + +func (p *ParallelsDesktopDevOps) MapObject() basetypes.ObjectValue { + attributeTypes := make(map[string]attr.Type) + attributeTypes["version"] = types.StringType + attributeTypes["protocol"] = types.StringType + attributeTypes["host"] = types.StringType + attributeTypes["port"] = types.StringType + attributeTypes["user"] = types.StringType + attributeTypes["password"] = types.StringType + + attrs := map[string]attr.Value{} + attrs["version"] = p.Version + attrs["protocol"] = p.Protocol + attrs["host"] = p.Host + attrs["port"] = p.Port + attrs["user"] = p.User + attrs["password"] = p.Password + + return types.ObjectValueMust(attributeTypes, attrs) +} + +func ApiConfigHasChanges(context context.Context, planState, currentState *ParallelsDesktopDevopsConfigV2) bool { + if planState == nil && currentState == nil { + return false + } + + if planState != nil && currentState == nil { + return true + } + + if planState == nil && currentState != nil { + return true + } + + if planState.Port != currentState.Port { + return true + } + + if planState.Prefix != currentState.Prefix { + return true + } + + if planState.DevOpsVersion != currentState.DevOpsVersion { + return true + } + + if planState.RootPassword != currentState.RootPassword { + return true + } + + if planState.HmacSecret != currentState.HmacSecret { + return true + } + + if planState.EncryptionRsaKey != currentState.EncryptionRsaKey { + return true + } + + if planState.LogLevel != currentState.LogLevel { + return true + } + + if planState.EnableTLS != currentState.EnableTLS { + return true + } + + if planState.TLSPort != currentState.TLSPort { + return true + } + + if planState.TLSCertificate != currentState.TLSCertificate { + return true + } + + if planState.TLSPrivateKey != currentState.TLSPrivateKey { + return true + } + + if planState.DisableCatalogCaching != currentState.DisableCatalogCaching { + return true + } + + if planState.TokenDurationMinutes != currentState.TokenDurationMinutes { + return true + } + + if planState.Mode != currentState.Mode { + return true + } + + if planState.UseOrchestratorResources != currentState.UseOrchestratorResources { + return true + } + + if planState.SystemReservedMemory != currentState.SystemReservedMemory { + return true + } + + if planState.SystemReservedCpu != currentState.SystemReservedCpu { + return true + } + + if planState.SystemReservedDisk != currentState.SystemReservedDisk { + return true + } + + if planState.EnableLogging != currentState.EnableLogging { + return true + } + + if len(planState.EnvironmentVariables) != len(currentState.EnvironmentVariables) { + return true + } + + if len(planState.EnvironmentVariables) != 0 { + for k, v := range planState.EnvironmentVariables { + if currentState.EnvironmentVariables[k] != v { + return true + } + } + } + + return false +} diff --git a/internal/deploy/models/resource_models_v0.go b/internal/deploy/models/resource_models_v0.go new file mode 100644 index 0000000..3058f3f --- /dev/null +++ b/internal/deploy/models/resource_models_v0.go @@ -0,0 +1,45 @@ +package models + +import ( + "terraform-provider-parallels-desktop/internal/schemas/orchestrator" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// DeployResourceModel describes the resource data model. + +type DeployResourceModelV0 struct { + SshConnection *DeployResourceSshConnection `tfsdk:"ssh_connection"` + CurrentVersion types.String `tfsdk:"current_version"` + CurrentPackerVersion types.String `tfsdk:"current_packer_version"` + CurrentVagrantVersion types.String `tfsdk:"current_vagrant_version"` + CurrentGitVersion types.String `tfsdk:"current_git_version"` + License types.Object `tfsdk:"license"` + Orchestrator *orchestrator.OrchestratorRegistration `tfsdk:"orchestrator_registration"` + ApiConfig *ParallelsDesktopDevopsConfigV0 `tfsdk:"api_config"` + Api types.Object `tfsdk:"api"` + InstalledDependencies types.List `tfsdk:"installed_dependencies"` + InstallLocal types.Bool `tfsdk:"install_local"` +} + +type ParallelsDesktopDevopsConfigV0 struct { + Port types.String `tfsdk:"port" json:"port,omitempty"` + Prefix types.String `tfsdk:"prefix" json:"prefix,omitempty"` + DevOpsVersion types.String `tfsdk:"devops_version" json:"devops_version,omitempty"` + RootPassword types.String `tfsdk:"root_password" json:"root_password,omitempty"` + HmacSecret types.String `tfsdk:"hmac_secret" json:"hmac_secret,omitempty"` + EncryptionRsaKey types.String `tfsdk:"encryption_rsa_key" json:"encryption_rsa_key,omitempty"` + LogLevel types.String `tfsdk:"log_level" json:"log_level,omitempty"` + EnableTLS types.Bool `tfsdk:"enable_tls" json:"enable_tls,omitempty"` + TLSPort types.String `tfsdk:"tls_port" json:"tls_port,omitempty"` + TLSCertificate types.String `tfsdk:"tls_certificate" json:"tls_certificate,omitempty"` + TLSPrivateKey types.String `tfsdk:"tls_private_key" json:"tls_private_key,omitempty"` + DisableCatalogCaching types.Bool `tfsdk:"disable_catalog_caching" json:"disable_catalog_caching,omitempty"` + TokenDurationMinutes types.String `tfsdk:"token_duration_minutes" json:"token_duration_minutes,omitempty"` + Mode types.String `tfsdk:"mode" json:"mode,omitempty"` + UseOrchestratorResources types.Bool `tfsdk:"use_orchestrator_resources"` + SystemReservedMemory types.String `tfsdk:"system_reserved_memory"` + SystemReservedCpu types.String `tfsdk:"system_reserved_cpu"` + SystemReservedDisk types.String `tfsdk:"system_reserved_disk"` + EnableLogging types.Bool `tfsdk:"enable_logging"` +} diff --git a/internal/deploy/models/resource_models_v1.go b/internal/deploy/models/resource_models_v1.go new file mode 100644 index 0000000..2c7e06d --- /dev/null +++ b/internal/deploy/models/resource_models_v1.go @@ -0,0 +1,99 @@ +package models + +import ( + "terraform-provider-parallels-desktop/internal/schemas/orchestrator" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// DeployResourceModel describes the resource data model. + +type DeployResourceModelV1 struct { + SshConnection *DeployResourceSshConnection `tfsdk:"ssh_connection"` + CurrentVersion types.String `tfsdk:"current_version"` + CurrentPackerVersion types.String `tfsdk:"current_packer_version"` + CurrentVagrantVersion types.String `tfsdk:"current_vagrant_version"` + CurrentGitVersion types.String `tfsdk:"current_git_version"` + License types.Object `tfsdk:"license"` + Orchestrator *orchestrator.OrchestratorRegistration `tfsdk:"orchestrator_registration"` + ApiConfig *ParallelsDesktopDevopsConfigV1 `tfsdk:"api_config"` + Api types.Object `tfsdk:"api"` + InstalledDependencies types.List `tfsdk:"installed_dependencies"` + InstallLocal types.Bool `tfsdk:"install_local"` +} + +type ParallelsDesktopDevopsConfigV1 struct { + Port types.String `tfsdk:"port" json:"port,omitempty"` + Prefix types.String `tfsdk:"prefix" json:"prefix,omitempty"` + DevOpsVersion types.String `tfsdk:"devops_version" json:"devops_version,omitempty"` + RootPassword types.String `tfsdk:"root_password" json:"root_password,omitempty"` + HmacSecret types.String `tfsdk:"hmac_secret" json:"hmac_secret,omitempty"` + EncryptionRsaKey types.String `tfsdk:"encryption_rsa_key" json:"encryption_rsa_key,omitempty"` + LogLevel types.String `tfsdk:"log_level" json:"log_level,omitempty"` + EnableTLS types.Bool `tfsdk:"enable_tls" json:"enable_tls,omitempty"` + TLSPort types.String `tfsdk:"tls_port" json:"tls_port,omitempty"` + TLSCertificate types.String `tfsdk:"tls_certificate" json:"tls_certificate,omitempty"` + TLSPrivateKey types.String `tfsdk:"tls_private_key" json:"tls_private_key,omitempty"` + DisableCatalogCaching types.Bool `tfsdk:"disable_catalog_caching" json:"disable_catalog_caching,omitempty"` + TokenDurationMinutes types.String `tfsdk:"token_duration_minutes" json:"token_duration_minutes,omitempty"` + Mode types.String `tfsdk:"mode" json:"mode,omitempty"` + UseOrchestratorResources types.Bool `tfsdk:"use_orchestrator_resources"` + SystemReservedMemory types.String `tfsdk:"system_reserved_memory"` + SystemReservedCpu types.String `tfsdk:"system_reserved_cpu"` + SystemReservedDisk types.String `tfsdk:"system_reserved_disk"` + EnableLogging types.Bool `tfsdk:"enable_logging"` + EnvironmentVariables map[string]types.String `tfsdk:"environment_variables"` +} + +func (p *ParallelsDesktopDevopsConfigV1) MapObject() basetypes.ObjectValue { + attributeTypes := make(map[string]attr.Type) + attributeTypes["port"] = types.StringType + attributeTypes["devops_version"] = types.StringType + attributeTypes["root_password"] = types.StringType + attributeTypes["hmac_secret"] = types.StringType + attributeTypes["encryption_rsa_key"] = types.StringType + attributeTypes["log_level"] = types.StringType + attributeTypes["enable_tls"] = types.BoolType + attributeTypes["tls_port"] = types.StringType + attributeTypes["tls_certificate"] = types.StringType + attributeTypes["tls_private_key"] = types.StringType + attributeTypes["disable_catalog_caching"] = types.BoolType + attributeTypes["token_duration_minutes"] = types.StringType + attributeTypes["mode"] = types.StringType + attributeTypes["use_orchestrator_resources"] = types.BoolType + attributeTypes["system_reserved_memory"] = types.StringType + attributeTypes["system_reserved_cpu"] = types.StringType + attributeTypes["system_reserved_disk"] = types.StringType + attributeTypes["enable_logging"] = types.BoolType + attributeTypes["environment_variables"] = types.MapType{} + + attrs := map[string]attr.Value{} + attrs["api_port"] = p.Port + attrs["devops_version"] = p.DevOpsVersion + attrs["root_password"] = p.RootPassword + attrs["hmac_secret"] = p.HmacSecret + attrs["encryption_rsa_key"] = p.EncryptionRsaKey + attrs["log_level"] = p.LogLevel + attrs["enable_tls"] = p.EnableTLS + attrs["host_tls_port"] = p.TLSPort + attrs["tls_certificate"] = p.TLSCertificate + attrs["tls_private_key"] = p.TLSPrivateKey + attrs["disable_catalog_caching"] = p.DisableCatalogCaching + attrs["token_duration_minutes"] = p.TokenDurationMinutes + attrs["mode"] = p.Mode + attrs["use_orchestrator_resources"] = p.UseOrchestratorResources + attrs["system_reserved_memory"] = p.SystemReservedMemory + attrs["system_reserved_cpu"] = p.SystemReservedCpu + attrs["system_reserved_disk"] = p.SystemReservedDisk + attrs["enable_logging"] = p.EnableLogging + + envVars := make(map[string]attr.Value) + for k, v := range p.EnvironmentVariables { + envVars[k] = v + } + attrs["environment_variables"] = types.MapValueMust(types.StringType, envVars) + + return types.ObjectValueMust(attributeTypes, attrs) +} diff --git a/internal/deploy/models/resource_models_v2.go b/internal/deploy/models/resource_models_v2.go new file mode 100644 index 0000000..85cf8f5 --- /dev/null +++ b/internal/deploy/models/resource_models_v2.go @@ -0,0 +1,142 @@ +package models + +import ( + "strings" + + "terraform-provider-parallels-desktop/internal/apiclient" + "terraform-provider-parallels-desktop/internal/models" + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/orchestrator" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// DeployResourceModel describes the resource data model. + +type DeployResourceModelV2 struct { + SshConnection *DeployResourceSshConnection `tfsdk:"ssh_connection"` + CurrentVersion types.String `tfsdk:"current_version"` + CurrentPackerVersion types.String `tfsdk:"current_packer_version"` + CurrentVagrantVersion types.String `tfsdk:"current_vagrant_version"` + CurrentGitVersion types.String `tfsdk:"current_git_version"` + ExternalIp types.String `tfsdk:"external_ip"` + License types.Object `tfsdk:"license"` + Orchestrator *orchestrator.OrchestratorRegistration `tfsdk:"orchestrator_registration"` + ReverseProxyHosts []*reverseproxy.ReverseProxyHost `tfsdk:"reverse_proxy_host"` + ApiConfig *ParallelsDesktopDevopsConfigV2 `tfsdk:"api_config"` + Api types.Object `tfsdk:"api"` + InstalledDependencies types.List `tfsdk:"installed_dependencies"` + InstallLocal types.Bool `tfsdk:"install_local"` + IsRegisteredInOrchestrator types.Bool `tfsdk:"is_registered_in_orchestrator"` + OrchestratorHost types.String `tfsdk:"orchestrator_host"` + OrchestratorHostId types.String `tfsdk:"orchestrator_host_id"` +} + +type ParallelsDesktopDevopsConfigV2 struct { + Port types.String `tfsdk:"port" json:"port,omitempty"` + Prefix types.String `tfsdk:"prefix" json:"prefix,omitempty"` + DevOpsVersion types.String `tfsdk:"devops_version" json:"devops_version,omitempty"` + RootPassword types.String `tfsdk:"root_password" json:"root_password,omitempty"` + HmacSecret types.String `tfsdk:"hmac_secret" json:"hmac_secret,omitempty"` + EncryptionRsaKey types.String `tfsdk:"encryption_rsa_key" json:"encryption_rsa_key,omitempty"` + LogLevel types.String `tfsdk:"log_level" json:"log_level,omitempty"` + EnableTLS types.Bool `tfsdk:"enable_tls" json:"enable_tls,omitempty"` + TLSPort types.String `tfsdk:"tls_port" json:"tls_port,omitempty"` + TLSCertificate types.String `tfsdk:"tls_certificate" json:"tls_certificate,omitempty"` + TLSPrivateKey types.String `tfsdk:"tls_private_key" json:"tls_private_key,omitempty"` + DisableCatalogCaching types.Bool `tfsdk:"disable_catalog_caching" json:"disable_catalog_caching,omitempty"` + TokenDurationMinutes types.String `tfsdk:"token_duration_minutes" json:"token_duration_minutes,omitempty"` + Mode types.String `tfsdk:"mode" json:"mode,omitempty"` + UseOrchestratorResources types.Bool `tfsdk:"use_orchestrator_resources"` + SystemReservedMemory types.String `tfsdk:"system_reserved_memory"` + SystemReservedCpu types.String `tfsdk:"system_reserved_cpu"` + SystemReservedDisk types.String `tfsdk:"system_reserved_disk"` + EnableLogging types.Bool `tfsdk:"enable_logging"` + EnablePortForwarding types.Bool `tfsdk:"enable_port_forwarding"` + UseLatestBeta types.Bool `tfsdk:"use_latest_beta"` + EnvironmentVariables map[string]types.String `tfsdk:"environment_variables"` +} + +func (p *ParallelsDesktopDevopsConfigV2) MapObject() basetypes.ObjectValue { + attributeTypes := make(map[string]attr.Type) + attributeTypes["port"] = types.StringType + attributeTypes["devops_version"] = types.StringType + attributeTypes["root_password"] = types.StringType + attributeTypes["hmac_secret"] = types.StringType + attributeTypes["encryption_rsa_key"] = types.StringType + attributeTypes["log_level"] = types.StringType + attributeTypes["enable_tls"] = types.BoolType + attributeTypes["tls_port"] = types.StringType + attributeTypes["tls_certificate"] = types.StringType + attributeTypes["tls_private_key"] = types.StringType + attributeTypes["disable_catalog_caching"] = types.BoolType + attributeTypes["token_duration_minutes"] = types.StringType + attributeTypes["mode"] = types.StringType + attributeTypes["use_orchestrator_resources"] = types.BoolType + attributeTypes["system_reserved_memory"] = types.StringType + attributeTypes["system_reserved_cpu"] = types.StringType + attributeTypes["system_reserved_disk"] = types.StringType + attributeTypes["enable_logging"] = types.BoolType + attributeTypes["enable_port_forwarding"] = types.BoolType + attributeTypes["use_latest_beta"] = types.BoolType + attributeTypes["environment_variables"] = types.MapType{} + + attrs := map[string]attr.Value{} + attrs["api_port"] = p.Port + attrs["devops_version"] = p.DevOpsVersion + attrs["root_password"] = p.RootPassword + attrs["hmac_secret"] = p.HmacSecret + attrs["encryption_rsa_key"] = p.EncryptionRsaKey + attrs["log_level"] = p.LogLevel + attrs["enable_tls"] = p.EnableTLS + attrs["host_tls_port"] = p.TLSPort + attrs["tls_certificate"] = p.TLSCertificate + attrs["tls_private_key"] = p.TLSPrivateKey + attrs["disable_catalog_caching"] = p.DisableCatalogCaching + attrs["token_duration_minutes"] = p.TokenDurationMinutes + attrs["mode"] = p.Mode + attrs["use_orchestrator_resources"] = p.UseOrchestratorResources + attrs["system_reserved_memory"] = p.SystemReservedMemory + attrs["system_reserved_cpu"] = p.SystemReservedCpu + attrs["system_reserved_disk"] = p.SystemReservedDisk + attrs["enable_logging"] = p.EnableLogging + attrs["enable_port_forwarding"] = p.EnablePortForwarding + attrs["use_latest_beta"] = p.UseLatestBeta + + envVars := make(map[string]attr.Value) + for k, v := range p.EnvironmentVariables { + envVars[k] = v + } + attrs["environment_variables"] = types.MapValueMust(types.StringType, envVars) + + return types.ObjectValueMust(attributeTypes, attrs) +} + +func (o *DeployResourceModelV2) GenerateApiHostConfig(provider *models.ParallelsProviderModel) apiclient.HostConfig { + hostConfig := apiclient.HostConfig{ + IsOrchestrator: false, + Host: strings.ReplaceAll(o.SshConnection.Host.String(), "\"", ""), + License: provider.License.ValueString(), + Authorization: &authenticator.Authentication{ + Username: types.StringValue(strings.ReplaceAll(o.Api.Attributes()["user"].String(), "\"", "")), + Password: types.StringValue(strings.ReplaceAll(o.Api.Attributes()["password"].String(), "\"", "")), + }, + + DisableTlsValidation: provider.DisableTlsValidation.ValueBool(), + } + api_port := strings.ReplaceAll(o.ApiConfig.Port.ValueString(), "\"", "") + api_schema := "http" + + if api_port != "" { + hostConfig.Host = hostConfig.Host + ":" + api_port + } + if o.ApiConfig.EnableTLS.ValueBool() { + api_schema = "https" + } + hostConfig.Host = api_schema + "://" + hostConfig.Host + + return hostConfig +} diff --git a/internal/deploy/resource.go b/internal/deploy/resource.go index 770cc11..554d138 100644 --- a/internal/deploy/resource.go +++ b/internal/deploy/resource.go @@ -6,11 +6,15 @@ import ( "fmt" "strings" + "terraform-provider-parallels-desktop/internal/common" + deploy_models "terraform-provider-parallels-desktop/internal/deploy/models" + "terraform-provider-parallels-desktop/internal/deploy/schemas" "terraform-provider-parallels-desktop/internal/interfaces" "terraform-provider-parallels-desktop/internal/localclient" "terraform-provider-parallels-desktop/internal/models" "terraform-provider-parallels-desktop/internal/schemas/authenticator" "terraform-provider-parallels-desktop/internal/schemas/orchestrator" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" "terraform-provider-parallels-desktop/internal/ssh" "terraform-provider-parallels-desktop/internal/telemetry" @@ -44,7 +48,7 @@ func (r *DeployResource) Metadata(ctx context.Context, req resource.MetadataRequ } func (r *DeployResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = deployResourceSchemaV1 + resp.Schema = schemas.DeployResourceSchemaV2 } func (r *DeployResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -67,7 +71,7 @@ func (r *DeployResource) Configure(ctx context.Context, req resource.ConfigureRe } func (r *DeployResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data DeployResourceModelV1 + var data deploy_models.DeployResourceModelV2 resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) telemetrySvc := telemetry.Get(ctx) @@ -161,28 +165,8 @@ func (r *DeployResource) Create(ctx context.Context, req resource.CreateRequest, // Register with orchestrator if needed if data.Orchestrator != nil { - apiData := data.Api - host := strings.ReplaceAll(apiData.Attributes()["host"].String(), "\"", "") - protocol := strings.ReplaceAll(apiData.Attributes()["protocol"].String(), "\"", "") - port := strings.ReplaceAll(apiData.Attributes()["port"].String(), "\"", "") - user := strings.ReplaceAll(apiData.Attributes()["user"].String(), "\"", "") - password := strings.ReplaceAll(apiData.Attributes()["password"].String(), "\"", "") - - orchestratorConfig := orchestrator.OrchestratorRegistration{ - HostId: data.Orchestrator.HostId, - Schema: types.StringValue(protocol), - Host: types.StringValue(host), - Port: types.StringValue(port), - Description: data.Orchestrator.Description, - Tags: data.Orchestrator.Tags, - HostCredentials: &authenticator.Authentication{ - Username: types.StringValue(user), - Password: types.StringValue(password), - }, - Orchestrator: data.Orchestrator.Orchestrator, - } - id, diag := orchestrator.RegisterWithHost(ctx, orchestratorConfig, r.provider.DisableTlsValidation.ValueBool()) + diag := r.registerWithOrchestrator(ctx, &data, nil) if diag.HasError() { if uninstallErrors := parallelsClient.UninstallDependencies(dependencies); len(uninstallErrors) > 0 { for _, uninstallError := range uninstallErrors { @@ -195,23 +179,14 @@ func (r *DeployResource) Create(ctx context.Context, req resource.CreateRequest, if err := parallelsClient.UninstallDevOpsService(); err != nil { resp.Diagnostics.AddError("Error uninstalling parallels DevOps service", err.Error()) } - isRegistered, diags := orchestrator.IsAlreadyRegistered(ctx, orchestratorConfig, r.provider.DisableTlsValidation.ValueBool()) + diags := r.unregisterWithOrchestrator(ctx, &data) if diags.HasError() { resp.Diagnostics.Append(diags...) return } - - if isRegistered { - if diag := orchestrator.UnregisterWithHost(ctx, orchestratorConfig, r.provider.DisableTlsValidation.ValueBool()); diag.HasError() { - resp.Diagnostics.Append(diag...) - } - } - resp.Diagnostics.Append(diag...) return } - - data.Orchestrator.HostId = types.StringValue(id) } var installedDependencies []attr.Value @@ -230,12 +205,32 @@ func (r *DeployResource) Create(ctx context.Context, req resource.CreateRequest, } data.InstalledDependencies = installDependenciesListValue + hostConfig := data.GenerateApiHostConfig(r.provider) + + if len(data.ReverseProxyHosts) > 0 { + rpHostsCopy := reverseproxy.CopyReverseProxyHosts(data.ReverseProxyHosts) + result, createDiag := reverseproxy.Create(ctx, hostConfig, rpHostsCopy) + if createDiag.HasError() { + resp.Diagnostics.Append(createDiag...) + if diag := reverseproxy.Delete(ctx, hostConfig, rpHostsCopy); diag.HasError() { + tflog.Error(ctx, "Error deleting reverse proxy hosts") + } + return + } + + for i := range result { + data.ReverseProxyHosts[i].ID = result[i].ID + } + } + + data.ExternalIp = types.StringValue(strings.ReplaceAll(data.SshConnection.Host.String(), "\"", "")) + // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *DeployResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data DeployResourceModelV1 + var data deploy_models.DeployResourceModelV2 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( ctx, @@ -285,7 +280,7 @@ func (r *DeployResource) Read(ctx context.Context, req resource.ReadRequest, res // Getting parallels latest api version if version, err := parallelsClient.GetDevOpsVersion(); err != nil { - planVersion := ParallelsDesktopDevOps{} + planVersion := deploy_models.ParallelsDesktopDevOps{} if !data.Api.IsNull() { if diags := data.Api.As(ctx, &planVersion, basetypes.ObjectAsOptions{}); diags.HasError() { resp.Diagnostics.Append(diags...) @@ -298,7 +293,7 @@ func (r *DeployResource) Read(ctx context.Context, req resource.ReadRequest, res data.Api = planVersion.MapObject() } } else { - planVersion := ParallelsDesktopDevOps{} + planVersion := deploy_models.ParallelsDesktopDevOps{} if !data.Api.IsNull() { if diags := data.Api.As(ctx, &planVersion, basetypes.ObjectAsOptions{}); diags.HasError() { resp.Diagnostics.Append(diags...) @@ -322,8 +317,8 @@ func (r *DeployResource) Read(ctx context.Context, req resource.ReadRequest, res } func (r *DeployResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data DeployResourceModelV1 - var currentData DeployResourceModelV1 + var data deploy_models.DeployResourceModelV2 + var currentData deploy_models.DeployResourceModelV2 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -375,7 +370,7 @@ func (r *DeployResource) Update(ctx context.Context, req resource.UpdateRequest, } // Check if the API config has changed - if ApiConfigHasChanges(ctx, data.ApiConfig, currentData.ApiConfig) { + if deploy_models.ApiConfigHasChanges(ctx, data.ApiConfig, currentData.ApiConfig) { if err := parallelsClient.UninstallDevOpsService(); err != nil { resp.Diagnostics.AddError("Error uninstalling parallels DevOps service", err.Error()) return @@ -384,6 +379,24 @@ func (r *DeployResource) Update(ctx context.Context, req resource.UpdateRequest, resp.Diagnostics.Append(diag...) return } + + if diag := r.registerWithOrchestrator(ctx, &data, ¤tData); diag.HasError() { + if uninstallErrors := parallelsClient.UninstallDependencies(dependencies); len(uninstallErrors) > 0 { + for _, uninstallError := range uninstallErrors { + diag.AddError("Error uninstalling dependencies", uninstallError.Error()) + } + } + if err := parallelsClient.UninstallParallelsDesktop(); err != nil { + resp.Diagnostics.AddError("Error uninstalling dependencies", err.Error()) + } + if err := parallelsClient.UninstallDevOpsService(); err != nil { + resp.Diagnostics.AddError("Error uninstalling parallels DevOps service", err.Error()) + } + resp.Diagnostics.Append(diag...) + return + } + + tflog.Info(ctx, "Changes in DevOps service, restarting parallels service") } // restart parallels service @@ -485,71 +498,6 @@ func (r *DeployResource) Update(ctx context.Context, req resource.UpdateRequest, data.InstalledDependencies = currentData.InstalledDependencies } - hasChangesInDevOpsService := false - if data.ApiConfig != nil { - if currentData.ApiConfig == nil { - hasChangesInDevOpsService = true - } else { - if data.ApiConfig.Port.ValueString() != currentData.ApiConfig.Port.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.TLSPort.ValueString() != currentData.ApiConfig.TLSPort.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.RootPassword.ValueString() != currentData.ApiConfig.RootPassword.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.EncryptionRsaKey.ValueString() != currentData.ApiConfig.EncryptionRsaKey.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.HmacSecret.ValueString() != currentData.ApiConfig.HmacSecret.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.LogLevel.ValueString() != currentData.ApiConfig.LogLevel.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.EnableTLS.ValueBool() != currentData.ApiConfig.EnableTLS.ValueBool() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.TLSPort.ValueString() != currentData.ApiConfig.TLSPort.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.TLSCertificate.ValueString() != currentData.ApiConfig.TLSCertificate.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.TLSPrivateKey.ValueString() != currentData.ApiConfig.TLSPrivateKey.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.DisableCatalogCaching.ValueBool() != currentData.ApiConfig.DisableCatalogCaching.ValueBool() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.TokenDurationMinutes.ValueString() != currentData.ApiConfig.TokenDurationMinutes.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.Mode.ValueString() != currentData.ApiConfig.Mode.ValueString() { - hasChangesInDevOpsService = true - } - if data.ApiConfig.UseOrchestratorResources.ValueBool() != currentData.ApiConfig.UseOrchestratorResources.ValueBool() { - hasChangesInDevOpsService = true - } - } - } - - if hasChangesInDevOpsService { - err := parallelsClient.UninstallDevOpsService() - if err != nil { - resp.Diagnostics.AddError("Error uninstalling parallels DevOps service", err.Error()) - return - } - if _, diag := r.installDevOpsService(&data, dependencies, parallelsClient); diag.HasError() { - resp.Diagnostics.Append(diag...) - return - } - tflog.Info(ctx, "Changes in DevOps service, restarting parallels service") - } else { - tflog.Info(ctx, "No changes in DevOps service") - } - installedVersion, getVersionError := parallelsClient.GetDevOpsVersion() if getVersionError != nil { if getVersionError.Error() == "Parallels Desktop DevOps Service not found" { @@ -588,53 +536,30 @@ func (r *DeployResource) Update(ctx context.Context, req resource.UpdateRequest, if data.Orchestrator != nil { if orchestrator.HasChanges(ctx, data.Orchestrator, currentData.Orchestrator) { - apiData := data.Api - host := strings.ReplaceAll(apiData.Attributes()["host"].String(), "\"", "") - protocol := strings.ReplaceAll(apiData.Attributes()["protocol"].String(), "\"", "") - port := strings.ReplaceAll(apiData.Attributes()["port"].String(), "\"", "") - user := strings.ReplaceAll(apiData.Attributes()["user"].String(), "\"", "") - password := strings.ReplaceAll(apiData.Attributes()["password"].String(), "\"", "") - - // checking if we already registered with orchestrator - if currentData.Orchestrator != nil && currentData.Orchestrator.HostId.ValueString() != "" { - if diag := orchestrator.UnregisterWithHost(ctx, *currentData.Orchestrator, r.provider.DisableTlsValidation.ValueBool()); diag.HasError() { - resp.Diagnostics.Append(diag...) - return + if diag := r.registerWithOrchestrator(ctx, &data, ¤tData); diag.HasError() { + if uninstallErrors := parallelsClient.UninstallDependencies(dependencies); len(uninstallErrors) > 0 { + for _, uninstallError := range uninstallErrors { + diag.AddError("Error uninstalling dependencies", uninstallError.Error()) + } } - } - - orchestratorConfig := orchestrator.OrchestratorRegistration{ - HostId: data.Orchestrator.HostId, - Schema: types.StringValue(protocol), - Host: types.StringValue(host), - Port: types.StringValue(port), - Description: data.Orchestrator.Description, - Tags: data.Orchestrator.Tags, - HostCredentials: &authenticator.Authentication{ - Username: types.StringValue(user), - Password: types.StringValue(password), - }, - Orchestrator: data.Orchestrator.Orchestrator, - } - - isRegistered, diags := orchestrator.IsAlreadyRegistered(ctx, orchestratorConfig, r.provider.DisableTlsValidation.ValueBool()) - if diags.HasError() { - resp.Diagnostics.Append(diags...) - return - } - if !isRegistered { - id, diag := orchestrator.RegisterWithHost(ctx, orchestratorConfig, r.provider.DisableTlsValidation.ValueBool()) - if diag.HasError() { - resp.Diagnostics.Append(diag...) - return + if err := parallelsClient.UninstallParallelsDesktop(); err != nil { + resp.Diagnostics.AddError("Error uninstalling dependencies", err.Error()) } - - data.Orchestrator.HostId = types.StringValue(id) - } else { - tflog.Info(ctx, "Already registered with orchestrator, skipping registration") + if err := parallelsClient.UninstallDevOpsService(); err != nil { + resp.Diagnostics.AddError("Error uninstalling parallels DevOps service", err.Error()) + } + resp.Diagnostics.Append(diag...) + return } } else { data.Orchestrator.HostId = currentData.Orchestrator.HostId + data.IsRegisteredInOrchestrator = types.BoolValue(true) + data.OrchestratorHost = currentData.OrchestratorHost + if common.GetString(data.OrchestratorHostId) != "" { + data.OrchestratorHostId = currentData.OrchestratorHostId + } else { + data.OrchestratorHostId = currentData.Orchestrator.HostId + } } } else if currentData.Orchestrator != nil { if currentData.Orchestrator.HostId.ValueString() != "" { @@ -645,6 +570,36 @@ func (r *DeployResource) Update(ctx context.Context, req resource.UpdateRequest, } } + hostConfig := data.GenerateApiHostConfig(r.provider) + + if reverseproxy.ReverseProxyHostsDiff(data.ReverseProxyHosts, currentData.ReverseProxyHosts) { + copyCurrentRpHosts := reverseproxy.CopyReverseProxyHosts(currentData.ReverseProxyHosts) + copyRpHosts := reverseproxy.CopyReverseProxyHosts(data.ReverseProxyHosts) + + results, updateDiag := reverseproxy.Update(ctx, hostConfig, copyCurrentRpHosts, copyRpHosts) + if updateDiag.HasError() { + resp.Diagnostics.Append(updateDiag...) + revertResults, _ := reverseproxy.Revert(ctx, hostConfig, copyCurrentRpHosts, copyRpHosts) + for i := range revertResults { + data.ReverseProxyHosts[i].ID = revertResults[i].ID + } + return + } + + for i := range results { + data.ReverseProxyHosts[i].ID = results[i].ID + } + } else { + for i := range currentData.ReverseProxyHosts { + data.ReverseProxyHosts[i].ID = currentData.ReverseProxyHosts[i].ID + } + } + + if currentData.ExternalIp.ValueString() == "" || + strings.ReplaceAll(currentData.ExternalIp.ValueString(), "\"", "") != strings.ReplaceAll(data.ExternalIp.ValueString(), "\"", "") { + data.ExternalIp = types.StringValue(strings.ReplaceAll(data.SshConnection.Host.String(), "\"", "")) + } + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) if resp.Diagnostics.HasError() { return @@ -652,7 +607,7 @@ func (r *DeployResource) Update(ctx context.Context, req resource.UpdateRequest, } func (r *DeployResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data DeployResourceModelV1 + var data deploy_models.DeployResourceModelV2 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -728,11 +683,21 @@ func (r *DeployResource) Delete(ctx context.Context, req resource.DeleteRequest, }) if data.Orchestrator != nil { - if diag := orchestrator.UnregisterWithHost(ctx, *data.Orchestrator, r.provider.DisableTlsValidation.ValueBool()); diag.HasError() { + if diag := r.unregisterWithOrchestrator(ctx, &data); diag.HasError() { resp.Diagnostics.Append(diag...) } } + hostConfig := data.GenerateApiHostConfig(r.provider) + + if len(data.ReverseProxyHosts) > 0 { + rpHostsCopy := reverseproxy.CopyReverseProxyHosts(data.ReverseProxyHosts) + if diag := reverseproxy.Delete(ctx, hostConfig, rpHostsCopy); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + } + if resp.Diagnostics.HasError() { return } @@ -745,21 +710,25 @@ func (r *DeployResource) ImportState(ctx context.Context, req resource.ImportSta func (r *DeployResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { return map[int64]resource.StateUpgrader{ 0: { - PriorSchema: &deployResourceSchemaV0, - StateUpgrader: UpgradeState, + PriorSchema: &schemas.DeployResourceSchemaV0, + StateUpgrader: UpgradeStateToV1, + }, + 1: { + PriorSchema: &schemas.DeployResourceSchemaV1, + StateUpgrader: UpgradeStateToV2, }, } } -func UpgradeState(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { - var priorStateData DeployResourceModelV0 +func UpgradeStateToV1(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData deploy_models.DeployResourceModelV0 resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) if resp.Diagnostics.HasError() { return } - upgradedStateData := DeployResourceModelV1{ + upgradedStateData := deploy_models.DeployResourceModelV1{ SshConnection: priorStateData.SshConnection, CurrentVersion: priorStateData.CurrentVersion, CurrentPackerVersion: priorStateData.CurrentPackerVersion, @@ -767,7 +736,7 @@ func UpgradeState(ctx context.Context, req resource.UpgradeStateRequest, resp *r CurrentGitVersion: priorStateData.CurrentGitVersion, License: priorStateData.License, Orchestrator: priorStateData.Orchestrator, - ApiConfig: &ParallelsDesktopDevopsConfigV1{ + ApiConfig: &deploy_models.ParallelsDesktopDevopsConfigV1{ Port: priorStateData.ApiConfig.Port, Prefix: priorStateData.ApiConfig.Prefix, DevOpsVersion: priorStateData.ApiConfig.DevOpsVersion, @@ -799,7 +768,58 @@ func UpgradeState(ctx context.Context, req resource.UpgradeStateRequest, resp *r resp.Diagnostics.Append(resp.State.Set(ctx, &upgradedStateData)...) } -func (r *DeployResource) getSshClient(data DeployResourceModelV1) (*ssh.SshClient, error) { +func UpgradeStateToV2(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData deploy_models.DeployResourceModelV1 + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := deploy_models.DeployResourceModelV2{ + SshConnection: priorStateData.SshConnection, + CurrentVersion: priorStateData.CurrentVersion, + CurrentPackerVersion: priorStateData.CurrentPackerVersion, + CurrentVagrantVersion: priorStateData.CurrentVagrantVersion, + CurrentGitVersion: priorStateData.CurrentGitVersion, + License: priorStateData.License, + Orchestrator: priorStateData.Orchestrator, + ApiConfig: &deploy_models.ParallelsDesktopDevopsConfigV2{ + Port: priorStateData.ApiConfig.Port, + Prefix: priorStateData.ApiConfig.Prefix, + DevOpsVersion: priorStateData.ApiConfig.DevOpsVersion, + RootPassword: priorStateData.ApiConfig.RootPassword, + HmacSecret: priorStateData.ApiConfig.HmacSecret, + EncryptionRsaKey: priorStateData.ApiConfig.EncryptionRsaKey, + LogLevel: priorStateData.ApiConfig.LogLevel, + EnableTLS: priorStateData.ApiConfig.EnableTLS, + TLSPort: priorStateData.ApiConfig.TLSPort, + TLSCertificate: priorStateData.ApiConfig.TLSCertificate, + TLSPrivateKey: priorStateData.ApiConfig.TLSPrivateKey, + DisableCatalogCaching: priorStateData.ApiConfig.DisableCatalogCaching, + TokenDurationMinutes: priorStateData.ApiConfig.TokenDurationMinutes, + Mode: priorStateData.ApiConfig.Mode, + UseOrchestratorResources: priorStateData.ApiConfig.UseOrchestratorResources, + SystemReservedMemory: priorStateData.ApiConfig.SystemReservedMemory, + SystemReservedCpu: priorStateData.ApiConfig.SystemReservedCpu, + SystemReservedDisk: priorStateData.ApiConfig.SystemReservedDisk, + EnableLogging: priorStateData.ApiConfig.EnableLogging, + EnvironmentVariables: priorStateData.ApiConfig.EnvironmentVariables, + EnablePortForwarding: basetypes.NewBoolValue(false), + UseLatestBeta: basetypes.NewBoolValue(false), + }, + ReverseProxyHosts: make([]*reverseproxy.ReverseProxyHost, 0), + Api: priorStateData.Api, + InstalledDependencies: priorStateData.InstalledDependencies, + InstallLocal: priorStateData.InstallLocal, + } + + println(fmt.Sprintf("Upgrading state from version %v", upgradedStateData)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &upgradedStateData)...) +} + +func (r *DeployResource) getSshClient(data deploy_models.DeployResourceModelV2) (*ssh.SshClient, error) { if data.SshConnection.Host.IsNull() { return nil, errors.New("host is required") } @@ -898,16 +918,16 @@ func (r *DeployResource) installParallelsDesktop(parallelsClient *DevOpsServiceC return installed_dependencies, diag } -func (r *DeployResource) installDevOpsService(data *DeployResourceModelV1, dependencies []string, parallelsClient *DevOpsServiceClient) (*ParallelsDesktopDevOps, diag.Diagnostics) { +func (r *DeployResource) installDevOpsService(data *deploy_models.DeployResourceModelV2, dependencies []string, parallelsClient *DevOpsServiceClient) (*deploy_models.ParallelsDesktopDevOps, diag.Diagnostics) { diag := diag.Diagnostics{} targetPort := "8080" targetTlsPort := "8443" apiVersion := "latest" // Installing parallels DevOps service - var config ParallelsDesktopDevopsConfigV1 + var config deploy_models.ParallelsDesktopDevopsConfigV2 if data.ApiConfig == nil { - config = ParallelsDesktopDevopsConfigV1{ + config = deploy_models.ParallelsDesktopDevopsConfigV2{ DevOpsVersion: types.StringValue(apiVersion), Port: types.StringValue(targetPort), TLSPort: types.StringValue(targetTlsPort), @@ -955,7 +975,7 @@ func (r *DeployResource) installDevOpsService(data *DeployResourceModelV1, depen return nil, diag } - apiData := ParallelsDesktopDevOps{ + apiData := deploy_models.ParallelsDesktopDevOps{ Version: types.StringValue(currentVersion), Host: types.StringValue(data.SshConnection.Host.ValueString()), Port: types.StringValue(targetPort), @@ -977,3 +997,130 @@ func (r *DeployResource) installDevOpsService(data *DeployResourceModelV1, depen return &apiData, diag } + +func (r *DeployResource) registerWithOrchestrator(ctx context.Context, data, currentData *deploy_models.DeployResourceModelV2) diag.Diagnostics { + diagnostic := diag.Diagnostics{} + if data.Orchestrator == nil { + return diagnostic + } + + host := strings.ReplaceAll(data.SshConnection.Host.String(), "\"", "") + port := strings.ReplaceAll(data.ApiConfig.Port.String(), "\"", "") + user := "root@localhost" + schema := "http" + password := strings.ReplaceAll(data.ApiConfig.RootPassword.String(), "\"", "") + if data.ApiConfig.EnableTLS.ValueBool() { + schema = "https" + } + + if currentData != nil { + currentRegistration := *currentData.Orchestrator + if common.GetString(currentData.OrchestratorHostId) != "" { + currentRegistration.HostId = currentData.OrchestratorHostId + } + if currentRegistration.HostId.ValueString() != "" && + currentData.Orchestrator != nil && + currentData.Orchestrator.HostId.ValueString() != "" { + currentRegistration.HostId = currentData.Orchestrator.HostId + } + + // checking if we already registered with orchestrator + isRegistered, item, diags := orchestrator.IsAlreadyRegistered(ctx, currentRegistration, r.provider.DisableTlsValidation.ValueBool()) + if diags.HasError() { + diagnostic.Append(diags...) + return diagnostic + } + if isRegistered { + currentRegistration.HostId = types.StringValue(item.ID) + if diag := orchestrator.UnregisterWithHost(ctx, currentRegistration, r.provider.DisableTlsValidation.ValueBool()); diag.HasError() { + diag.Append(diag...) + return diag + } + } + } + + // New registration details + orchestratorConfig := orchestrator.OrchestratorRegistration{ + HostId: data.Orchestrator.HostId, + Schema: types.StringValue(schema), + Host: types.StringValue(host), + Port: types.StringValue(port), + Description: data.Orchestrator.Description, + Tags: data.Orchestrator.Tags, + HostCredentials: &authenticator.Authentication{ + Username: types.StringValue(user), + Password: types.StringValue(password), + }, + Orchestrator: data.Orchestrator.Orchestrator, + } + + isRegistered, item, diags := orchestrator.IsAlreadyRegistered(ctx, orchestratorConfig, r.provider.DisableTlsValidation.ValueBool()) + if diags.HasError() { + diagnostic.Append(diags...) + data.IsRegisteredInOrchestrator = types.BoolValue(true) + data.OrchestratorHostId = types.StringValue(item.ID) + data.OrchestratorHost = types.StringValue(item.Host) + return diagnostic + } + + if !isRegistered { + id, diag := orchestrator.RegisterWithHost(ctx, orchestratorConfig, r.provider.DisableTlsValidation.ValueBool()) + if diag.HasError() { + diagnostic.Append(diag...) + return diagnostic + } + + if data.Orchestrator != nil { + data.Orchestrator.HostId = types.StringValue(id) + } + data.IsRegisteredInOrchestrator = types.BoolValue(true) + data.OrchestratorHostId = types.StringValue(id) + data.OrchestratorHost = types.StringValue(orchestratorConfig.GetHost()) + } else { + tflog.Info(ctx, "Already registered with orchestrator, skipping registration") + if data.Orchestrator != nil { + data.Orchestrator.HostId = types.StringValue(item.ID) + } + data.IsRegisteredInOrchestrator = types.BoolValue(true) + data.OrchestratorHostId = types.StringValue(item.ID) + data.OrchestratorHost = types.StringValue(item.Host) + } + + return diagnostic +} + +func (r *DeployResource) unregisterWithOrchestrator(ctx context.Context, data *deploy_models.DeployResourceModelV2) diag.Diagnostics { + diagnostic := diag.Diagnostics{} + if data.Orchestrator == nil { + return diagnostic + } + + currentRegistration := *data.Orchestrator + if common.GetString(data.OrchestratorHostId) != "" { + currentRegistration.HostId = data.OrchestratorHostId + } + + isRegistered, item, diags := orchestrator.IsAlreadyRegistered(ctx, currentRegistration, r.provider.DisableTlsValidation.ValueBool()) + if diags.HasError() { + diagnostic.Append(diags...) + return diagnostic + } + + if isRegistered { + // checking if we already registered with orchestrator + currentRegistration.HostId = types.StringValue(item.ID) + if diag := orchestrator.UnregisterWithHost(ctx, currentRegistration, r.provider.DisableTlsValidation.ValueBool()); diag.HasError() { + diag.Append(diag...) + return diag + } + } + if data.Orchestrator != nil { + data.Orchestrator.HostId = types.StringValue("") + } + + data.IsRegisteredInOrchestrator = types.BoolValue(false) + data.OrchestratorHostId = types.StringValue("") + data.OrchestratorHost = types.StringValue("") + + return diagnostic +} diff --git a/internal/deploy/resource_models.go b/internal/deploy/resource_models.go deleted file mode 100644 index 0e44837..0000000 --- a/internal/deploy/resource_models.go +++ /dev/null @@ -1,305 +0,0 @@ -package deploy - -import ( - "context" - - "terraform-provider-parallels-desktop/internal/clientmodels" - "terraform-provider-parallels-desktop/internal/schemas/orchestrator" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" -) - -// DeployResourceModel describes the resource data model. - -type DeployResourceModelV0 struct { - SshConnection *DeployResourceSshConnection `tfsdk:"ssh_connection"` - CurrentVersion types.String `tfsdk:"current_version"` - CurrentPackerVersion types.String `tfsdk:"current_packer_version"` - CurrentVagrantVersion types.String `tfsdk:"current_vagrant_version"` - CurrentGitVersion types.String `tfsdk:"current_git_version"` - License types.Object `tfsdk:"license"` - Orchestrator *orchestrator.OrchestratorRegistration `tfsdk:"orchestrator_registration"` - ApiConfig *ParallelsDesktopDevopsConfigV0 `tfsdk:"api_config"` - Api types.Object `tfsdk:"api"` - InstalledDependencies types.List `tfsdk:"installed_dependencies"` - InstallLocal types.Bool `tfsdk:"install_local"` -} - -type DeployResourceModelV1 struct { - SshConnection *DeployResourceSshConnection `tfsdk:"ssh_connection"` - CurrentVersion types.String `tfsdk:"current_version"` - CurrentPackerVersion types.String `tfsdk:"current_packer_version"` - CurrentVagrantVersion types.String `tfsdk:"current_vagrant_version"` - CurrentGitVersion types.String `tfsdk:"current_git_version"` - License types.Object `tfsdk:"license"` - Orchestrator *orchestrator.OrchestratorRegistration `tfsdk:"orchestrator_registration"` - ApiConfig *ParallelsDesktopDevopsConfigV1 `tfsdk:"api_config"` - Api types.Object `tfsdk:"api"` - InstalledDependencies types.List `tfsdk:"installed_dependencies"` - InstallLocal types.Bool `tfsdk:"install_local"` -} - -type DeployResourceSshConnection struct { - Host types.String `tfsdk:"host"` - HostPort types.String `tfsdk:"host_port"` - User types.String `tfsdk:"user"` - Password types.String `tfsdk:"password"` - PrivateKey types.String `tfsdk:"private_key"` -} - -type ParallelsDesktopLicense struct { - State types.String `tfsdk:"state"` - Key types.String `tfsdk:"key"` - Restricted types.Bool `tfsdk:"restricted"` -} - -func (p *ParallelsDesktopLicense) FromClientModel(value clientmodels.ParallelsServerLicense) { - p.State = types.StringValue(value.State) - p.Key = types.StringValue(value.Key) - p.Restricted = types.BoolValue(value.Restricted == "true") -} - -func (p *ParallelsDesktopLicense) MapObject() basetypes.ObjectValue { - attributeTypes := make(map[string]attr.Type) - attributeTypes["state"] = types.StringType - attributeTypes["key"] = types.StringType - attributeTypes["restricted"] = types.BoolType - - attrs := map[string]attr.Value{} - attrs["state"] = p.State - attrs["key"] = p.Key - attrs["restricted"] = p.Restricted - - return types.ObjectValueMust(attributeTypes, attrs) -} - -type ParallelsDesktopDevOps struct { - Version types.String `tfsdk:"version"` - Protocol types.String `tfsdk:"protocol"` - Host types.String `tfsdk:"host"` - Port types.String `tfsdk:"port"` - User types.String `tfsdk:"user"` - Password types.String `tfsdk:"password"` -} - -func (p *ParallelsDesktopDevOps) MapObject() basetypes.ObjectValue { - attributeTypes := make(map[string]attr.Type) - attributeTypes["version"] = types.StringType - attributeTypes["protocol"] = types.StringType - attributeTypes["host"] = types.StringType - attributeTypes["port"] = types.StringType - attributeTypes["user"] = types.StringType - attributeTypes["password"] = types.StringType - - attrs := map[string]attr.Value{} - attrs["version"] = p.Version - attrs["protocol"] = p.Protocol - attrs["host"] = p.Host - attrs["port"] = p.Port - attrs["user"] = p.User - attrs["password"] = p.Password - - return types.ObjectValueMust(attributeTypes, attrs) -} - -type ParallelsDesktopDevopsConfigV0 struct { - Port types.String `tfsdk:"port" json:"port,omitempty"` - Prefix types.String `tfsdk:"prefix" json:"prefix,omitempty"` - DevOpsVersion types.String `tfsdk:"devops_version" json:"devops_version,omitempty"` - RootPassword types.String `tfsdk:"root_password" json:"root_password,omitempty"` - HmacSecret types.String `tfsdk:"hmac_secret" json:"hmac_secret,omitempty"` - EncryptionRsaKey types.String `tfsdk:"encryption_rsa_key" json:"encryption_rsa_key,omitempty"` - LogLevel types.String `tfsdk:"log_level" json:"log_level,omitempty"` - EnableTLS types.Bool `tfsdk:"enable_tls" json:"enable_tls,omitempty"` - TLSPort types.String `tfsdk:"tls_port" json:"tls_port,omitempty"` - TLSCertificate types.String `tfsdk:"tls_certificate" json:"tls_certificate,omitempty"` - TLSPrivateKey types.String `tfsdk:"tls_private_key" json:"tls_private_key,omitempty"` - DisableCatalogCaching types.Bool `tfsdk:"disable_catalog_caching" json:"disable_catalog_caching,omitempty"` - TokenDurationMinutes types.String `tfsdk:"token_duration_minutes" json:"token_duration_minutes,omitempty"` - Mode types.String `tfsdk:"mode" json:"mode,omitempty"` - UseOrchestratorResources types.Bool `tfsdk:"use_orchestrator_resources"` - SystemReservedMemory types.String `tfsdk:"system_reserved_memory"` - SystemReservedCpu types.String `tfsdk:"system_reserved_cpu"` - SystemReservedDisk types.String `tfsdk:"system_reserved_disk"` - EnableLogging types.Bool `tfsdk:"enable_logging"` -} - -type ParallelsDesktopDevopsConfigV1 struct { - Port types.String `tfsdk:"port" json:"port,omitempty"` - Prefix types.String `tfsdk:"prefix" json:"prefix,omitempty"` - DevOpsVersion types.String `tfsdk:"devops_version" json:"devops_version,omitempty"` - RootPassword types.String `tfsdk:"root_password" json:"root_password,omitempty"` - HmacSecret types.String `tfsdk:"hmac_secret" json:"hmac_secret,omitempty"` - EncryptionRsaKey types.String `tfsdk:"encryption_rsa_key" json:"encryption_rsa_key,omitempty"` - LogLevel types.String `tfsdk:"log_level" json:"log_level,omitempty"` - EnableTLS types.Bool `tfsdk:"enable_tls" json:"enable_tls,omitempty"` - TLSPort types.String `tfsdk:"tls_port" json:"tls_port,omitempty"` - TLSCertificate types.String `tfsdk:"tls_certificate" json:"tls_certificate,omitempty"` - TLSPrivateKey types.String `tfsdk:"tls_private_key" json:"tls_private_key,omitempty"` - DisableCatalogCaching types.Bool `tfsdk:"disable_catalog_caching" json:"disable_catalog_caching,omitempty"` - TokenDurationMinutes types.String `tfsdk:"token_duration_minutes" json:"token_duration_minutes,omitempty"` - Mode types.String `tfsdk:"mode" json:"mode,omitempty"` - UseOrchestratorResources types.Bool `tfsdk:"use_orchestrator_resources"` - SystemReservedMemory types.String `tfsdk:"system_reserved_memory"` - SystemReservedCpu types.String `tfsdk:"system_reserved_cpu"` - SystemReservedDisk types.String `tfsdk:"system_reserved_disk"` - EnableLogging types.Bool `tfsdk:"enable_logging"` - EnvironmentVariables map[string]types.String `tfsdk:"environment_variables"` -} - -func (p *ParallelsDesktopDevopsConfigV1) MapObject() basetypes.ObjectValue { - attributeTypes := make(map[string]attr.Type) - attributeTypes["port"] = types.StringType - attributeTypes["devops_version"] = types.StringType - attributeTypes["root_password"] = types.StringType - attributeTypes["hmac_secret"] = types.StringType - attributeTypes["encryption_rsa_key"] = types.StringType - attributeTypes["log_level"] = types.StringType - attributeTypes["enable_tls"] = types.BoolType - attributeTypes["tls_port"] = types.StringType - attributeTypes["tls_certificate"] = types.StringType - attributeTypes["tls_private_key"] = types.StringType - attributeTypes["disable_catalog_caching"] = types.BoolType - attributeTypes["token_duration_minutes"] = types.StringType - attributeTypes["mode"] = types.StringType - attributeTypes["use_orchestrator_resources"] = types.BoolType - attributeTypes["system_reserved_memory"] = types.StringType - attributeTypes["system_reserved_cpu"] = types.StringType - attributeTypes["system_reserved_disk"] = types.StringType - attributeTypes["enable_logging"] = types.BoolType - attributeTypes["environment_variables"] = types.MapType{} - - attrs := map[string]attr.Value{} - attrs["api_port"] = p.Port - attrs["devops_version"] = p.DevOpsVersion - attrs["root_password"] = p.RootPassword - attrs["hmac_secret"] = p.HmacSecret - attrs["encryption_rsa_key"] = p.EncryptionRsaKey - attrs["log_level"] = p.LogLevel - attrs["enable_tls"] = p.EnableTLS - attrs["host_tls_port"] = p.TLSPort - attrs["tls_certificate"] = p.TLSCertificate - attrs["tls_private_key"] = p.TLSPrivateKey - attrs["disable_catalog_caching"] = p.DisableCatalogCaching - attrs["token_duration_minutes"] = p.TokenDurationMinutes - attrs["mode"] = p.Mode - attrs["use_orchestrator_resources"] = p.UseOrchestratorResources - attrs["system_reserved_memory"] = p.SystemReservedMemory - attrs["system_reserved_cpu"] = p.SystemReservedCpu - attrs["system_reserved_disk"] = p.SystemReservedDisk - attrs["enable_logging"] = p.EnableLogging - - envVars := make(map[string]attr.Value) - for k, v := range p.EnvironmentVariables { - envVars[k] = v - } - attrs["environment_variables"] = types.MapValueMust(types.StringType, envVars) - - return types.ObjectValueMust(attributeTypes, attrs) -} - -func ApiConfigHasChanges(context context.Context, planState, currentState *ParallelsDesktopDevopsConfigV1) bool { - if planState == nil && currentState == nil { - return false - } - - if planState != nil && currentState == nil { - return true - } - - if planState == nil && currentState != nil { - return true - } - - if planState.Port != currentState.Port { - return true - } - - if planState.Prefix != currentState.Prefix { - return true - } - - if planState.DevOpsVersion != currentState.DevOpsVersion { - return true - } - - if planState.RootPassword != currentState.RootPassword { - return true - } - - if planState.HmacSecret != currentState.HmacSecret { - return true - } - - if planState.EncryptionRsaKey != currentState.EncryptionRsaKey { - return true - } - - if planState.LogLevel != currentState.LogLevel { - return true - } - - if planState.EnableTLS != currentState.EnableTLS { - return true - } - - if planState.TLSPort != currentState.TLSPort { - return true - } - - if planState.TLSCertificate != currentState.TLSCertificate { - return true - } - - if planState.TLSPrivateKey != currentState.TLSPrivateKey { - return true - } - - if planState.DisableCatalogCaching != currentState.DisableCatalogCaching { - return true - } - - if planState.TokenDurationMinutes != currentState.TokenDurationMinutes { - return true - } - - if planState.Mode != currentState.Mode { - return true - } - - if planState.UseOrchestratorResources != currentState.UseOrchestratorResources { - return true - } - - if planState.SystemReservedMemory != currentState.SystemReservedMemory { - return true - } - - if planState.SystemReservedCpu != currentState.SystemReservedCpu { - return true - } - - if planState.SystemReservedDisk != currentState.SystemReservedDisk { - return true - } - - if planState.EnableLogging != currentState.EnableLogging { - return true - } - - if len(planState.EnvironmentVariables) != len(currentState.EnvironmentVariables) { - return true - } - - if len(planState.EnvironmentVariables) != 0 { - for k, v := range planState.EnvironmentVariables { - if currentState.EnvironmentVariables[k] != v { - return true - } - } - } - - return false -} diff --git a/internal/deploy/schemas/api_config_schema_v0.go b/internal/deploy/schemas/api_config_schema_v0.go new file mode 100644 index 0000000..a89d867 --- /dev/null +++ b/internal/deploy/schemas/api_config_schema_v0.go @@ -0,0 +1,115 @@ +package schemas + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var ApiConfigSchemaBlockV0 = schema.SingleNestedBlock{ + MarkdownDescription: "Parallels Desktop DevOps configuration", + Description: "Parallels Desktop DevOps configuration", + Attributes: map[string]schema.Attribute{ + "port": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps port", + Description: "Parallels Desktop DevOps port", + Optional: true, + }, + "prefix": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps port", + Description: "Parallels Desktop DevOps port", + Optional: true, + }, + "devops_version": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps version to install, if empty the latest will be installed", + Description: "Parallels Desktop DevOps version to install, if empty the latest will be installed", + Optional: true, + }, + "root_password": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps root password", + Description: "Parallels Desktop DevOps root password", + Optional: true, + Sensitive: true, + }, + "hmac_secret": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", + Description: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", + Optional: true, + Sensitive: true, + }, + "encryption_rsa_key": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", + Description: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", + Optional: true, + Sensitive: true, + }, + "log_level": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", + Description: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("debug", "info", "warn", "error"), + }, + }, + "enable_tls": schema.BoolAttribute{ + MarkdownDescription: "Parallels Desktop DevOps enable TLS", + Description: "Parallels Desktop DevOps enable TLS", + Optional: true, + }, + "tls_port": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS port", + Description: "Parallels Desktop DevOps TLS port", + Optional: true, + }, + "tls_certificate": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", + Description: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", + Optional: true, + Sensitive: true, + }, + "tls_private_key": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", + Description: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", + Optional: true, + Sensitive: true, + }, + "disable_catalog_caching": schema.BoolAttribute{ + MarkdownDescription: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", + Description: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", + Optional: true, + }, + "token_duration_minutes": schema.StringAttribute{ + MarkdownDescription: "JWT Token duration in minutes", + Description: "JWT Token duration in minutes", + Optional: true, + }, + "mode": schema.StringAttribute{ + MarkdownDescription: "API Operation mode, either orchestrator or catalog", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.OneOf("orchestrator", "catalog", "api"), + }, + }, + "use_orchestrator_resources": schema.BoolAttribute{ + MarkdownDescription: "Use orchestrator resources", + Optional: true, + }, + "system_reserved_memory": schema.StringAttribute{ + MarkdownDescription: "System reserved memory in MB", + Optional: true, + }, + "system_reserved_cpu": schema.StringAttribute{ + MarkdownDescription: "System reserved CPU in %", + Optional: true, + }, + "system_reserved_disk": schema.StringAttribute{ + MarkdownDescription: "System reserved disk in MB", + Optional: true, + }, + "enable_logging": schema.BoolAttribute{ + MarkdownDescription: "Enable logging", + Optional: true, + }, + }, +} diff --git a/internal/deploy/schemas/api_config_schema_v1.go b/internal/deploy/schemas/api_config_schema_v1.go new file mode 100644 index 0000000..4bab6d5 --- /dev/null +++ b/internal/deploy/schemas/api_config_schema_v1.go @@ -0,0 +1,121 @@ +package schemas + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ApiConfigSchemaBlockV1 = schema.SingleNestedBlock{ + MarkdownDescription: "Parallels Desktop DevOps configuration", + Description: "Parallels Desktop DevOps configuration", + Attributes: map[string]schema.Attribute{ + "port": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps port", + Description: "Parallels Desktop DevOps port", + Optional: true, + }, + "prefix": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps port", + Description: "Parallels Desktop DevOps port", + Optional: true, + }, + "devops_version": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps version to install, if empty the latest will be installed", + Description: "Parallels Desktop DevOps version to install, if empty the latest will be installed", + Optional: true, + }, + "root_password": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps root password", + Description: "Parallels Desktop DevOps root password", + Optional: true, + Sensitive: true, + }, + "hmac_secret": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", + Description: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", + Optional: true, + Sensitive: true, + }, + "encryption_rsa_key": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", + Description: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", + Optional: true, + Sensitive: true, + }, + "log_level": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", + Description: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("debug", "info", "warn", "error"), + }, + }, + "enable_tls": schema.BoolAttribute{ + MarkdownDescription: "Parallels Desktop DevOps enable TLS", + Description: "Parallels Desktop DevOps enable TLS", + Optional: true, + }, + "tls_port": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS port", + Description: "Parallels Desktop DevOps TLS port", + Optional: true, + }, + "tls_certificate": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", + Description: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", + Optional: true, + Sensitive: true, + }, + "tls_private_key": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", + Description: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", + Optional: true, + Sensitive: true, + }, + "disable_catalog_caching": schema.BoolAttribute{ + MarkdownDescription: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", + Description: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", + Optional: true, + }, + "token_duration_minutes": schema.StringAttribute{ + MarkdownDescription: "JWT Token duration in minutes", + Description: "JWT Token duration in minutes", + Optional: true, + }, + "mode": schema.StringAttribute{ + MarkdownDescription: "API Operation mode, either orchestrator or catalog", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.OneOf("orchestrator", "catalog", "api"), + }, + }, + "use_orchestrator_resources": schema.BoolAttribute{ + MarkdownDescription: "Use orchestrator resources", + Optional: true, + }, + "system_reserved_memory": schema.StringAttribute{ + MarkdownDescription: "System reserved memory in MB", + Optional: true, + }, + "system_reserved_cpu": schema.StringAttribute{ + MarkdownDescription: "System reserved CPU in %", + Optional: true, + }, + "system_reserved_disk": schema.StringAttribute{ + MarkdownDescription: "System reserved disk in MB", + Optional: true, + }, + "enable_logging": schema.BoolAttribute{ + MarkdownDescription: "Enable logging", + Optional: true, + }, + "environment_variables": schema.MapAttribute{ + MarkdownDescription: "Environment variables that can be used in the DevOps service, please see documentation to see which variables are available", + Optional: true, + ElementType: types.StringType, + }, + }, +} diff --git a/internal/deploy/schemas/api_config_schema_v2.go b/internal/deploy/schemas/api_config_schema_v2.go new file mode 100644 index 0000000..4c03e24 --- /dev/null +++ b/internal/deploy/schemas/api_config_schema_v2.go @@ -0,0 +1,129 @@ +package schemas + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ApiConfigSchemaBlockV2 = schema.SingleNestedBlock{ + MarkdownDescription: "Parallels Desktop DevOps configuration", + Description: "Parallels Desktop DevOps configuration", + Attributes: map[string]schema.Attribute{ + "port": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps port", + Description: "Parallels Desktop DevOps port", + Optional: true, + }, + "prefix": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps port", + Description: "Parallels Desktop DevOps port", + Optional: true, + }, + "devops_version": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps version to install, if empty the latest will be installed", + Description: "Parallels Desktop DevOps version to install, if empty the latest will be installed", + Optional: true, + }, + "use_latest_beta": schema.BoolAttribute{ + MarkdownDescription: "Enables the use of the latest beta", + Optional: true, + }, + "root_password": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps root password", + Description: "Parallels Desktop DevOps root password", + Optional: true, + Sensitive: true, + }, + "hmac_secret": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", + Description: "Parallels Desktop DevOps HMAC secret, this is used to sign the JWT tokens", + Optional: true, + Sensitive: true, + }, + "encryption_rsa_key": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", + Description: "Parallels Desktop DevOps RSA key, this is used to encrypt database file on rest", + Optional: true, + Sensitive: true, + }, + "log_level": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", + Description: "Parallels Desktop DevOps log level, you can choose between debug, info, warn, error", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("debug", "info", "warn", "error"), + }, + }, + "enable_tls": schema.BoolAttribute{ + MarkdownDescription: "Parallels Desktop DevOps enable TLS", + Description: "Parallels Desktop DevOps enable TLS", + Optional: true, + }, + "tls_port": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS port", + Description: "Parallels Desktop DevOps TLS port", + Optional: true, + }, + "tls_certificate": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", + Description: "Parallels Desktop DevOps TLS certificate, this should be a PEM base64 encoded certificate string", + Optional: true, + Sensitive: true, + }, + "tls_private_key": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", + Description: "Parallels Desktop DevOps TLS private key, this should be a PEM base64 encoded private key string", + Optional: true, + Sensitive: true, + }, + "disable_catalog_caching": schema.BoolAttribute{ + MarkdownDescription: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", + Description: "Disable catalog caching, this will disable the ability to cache catalog items that are pulled from a remote catalog", + Optional: true, + }, + "token_duration_minutes": schema.StringAttribute{ + MarkdownDescription: "JWT Token duration in minutes", + Description: "JWT Token duration in minutes", + Optional: true, + }, + "mode": schema.StringAttribute{ + MarkdownDescription: "API Operation mode, either orchestrator or catalog", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.OneOf("orchestrator", "catalog", "api"), + }, + }, + "use_orchestrator_resources": schema.BoolAttribute{ + MarkdownDescription: "Use orchestrator resources", + Optional: true, + }, + "system_reserved_memory": schema.StringAttribute{ + MarkdownDescription: "System reserved memory in MB", + Optional: true, + }, + "system_reserved_cpu": schema.StringAttribute{ + MarkdownDescription: "System reserved CPU in %", + Optional: true, + }, + "system_reserved_disk": schema.StringAttribute{ + MarkdownDescription: "System reserved disk in MB", + Optional: true, + }, + "enable_logging": schema.BoolAttribute{ + MarkdownDescription: "Enable logging", + Optional: true, + }, + "environment_variables": schema.MapAttribute{ + MarkdownDescription: "Environment variables that can be used in the DevOps service, please see documentation to see which variables are available", + Optional: true, + ElementType: types.StringType, + }, + "enable_port_forwarding": schema.BoolAttribute{ + MarkdownDescription: "Enable inbuilt reverse proxy for port forwarding", + Optional: true, + }, + }, +} diff --git a/internal/deploy/schemas/resource_schema_v0.go b/internal/deploy/schemas/resource_schema_v0.go new file mode 100644 index 0000000..59ff307 --- /dev/null +++ b/internal/deploy/schemas/resource_schema_v0.go @@ -0,0 +1,86 @@ +package schemas + +import ( + "terraform-provider-parallels-desktop/internal/schemas/orchestrator" + "terraform-provider-parallels-desktop/internal/schemas/sshconnection" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var DeployResourceSchemaV0 = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Parallels Virtual Machine Deployment Resource", + Blocks: map[string]schema.Block{ + ApiConfigSchemaName: ApiConfigSchemaBlockV0, + sshconnection.SchemaName: sshconnection.SchemaBlockV0, + orchestrator.SchemaName: orchestrator.SchemaBlockV0, + }, + Version: 1, + Attributes: map[string]schema.Attribute{ + "current_version": schema.StringAttribute{ + MarkdownDescription: "Current version of Parallels Desktop", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "current_packer_version": schema.StringAttribute{ + MarkdownDescription: "Current version of Hashicorp Packer", + Description: "Current version of Hashicorp Packer", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "current_vagrant_version": schema.StringAttribute{ + MarkdownDescription: "Current version of Hashicorp Vagrant", + Description: "Current version of Hashicorp Vagrant", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "current_git_version": schema.StringAttribute{ + MarkdownDescription: "Current version of Git", + Description: "Current version of Git", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "license": schema.ObjectAttribute{ + MarkdownDescription: "Parallels Desktop license", + Computed: true, + AttributeTypes: map[string]attr.Type{ + "state": types.StringType, + "key": types.StringType, + "restricted": types.BoolType, + }, + }, + "api": schema.ObjectAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Service", + Computed: true, + AttributeTypes: map[string]attr.Type{ + "version": types.StringType, + "protocol": types.StringType, + "host": types.StringType, + "port": types.StringType, + "user": types.StringType, + "password": types.StringType, + }, + }, + "installed_dependencies": schema.ListAttribute{ + MarkdownDescription: "List of installed dependencies", + Computed: true, + ElementType: types.StringType, + }, + "install_local": schema.BoolAttribute{ + MarkdownDescription: "Deploy Parallels Desktop in the local machine, this will ignore the need to connect to a remote machine", + Optional: true, + }, + }, +} diff --git a/internal/deploy/schemas/resource_schema_v1.go b/internal/deploy/schemas/resource_schema_v1.go new file mode 100644 index 0000000..b4ba953 --- /dev/null +++ b/internal/deploy/schemas/resource_schema_v1.go @@ -0,0 +1,86 @@ +package schemas + +import ( + "terraform-provider-parallels-desktop/internal/schemas/orchestrator" + "terraform-provider-parallels-desktop/internal/schemas/sshconnection" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var DeployResourceSchemaV1 = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Parallels Virtual Machine Deployment Resource", + Blocks: map[string]schema.Block{ + ApiConfigSchemaName: ApiConfigSchemaBlockV1, + sshconnection.SchemaName: sshconnection.SchemaBlockV0, + orchestrator.SchemaName: orchestrator.SchemaBlockV0, + }, + Version: 1, + Attributes: map[string]schema.Attribute{ + "current_version": schema.StringAttribute{ + MarkdownDescription: "Current version of Parallels Desktop", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "current_packer_version": schema.StringAttribute{ + MarkdownDescription: "Current version of Hashicorp Packer", + Description: "Current version of Hashicorp Packer", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "current_vagrant_version": schema.StringAttribute{ + MarkdownDescription: "Current version of Hashicorp Vagrant", + Description: "Current version of Hashicorp Vagrant", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "current_git_version": schema.StringAttribute{ + MarkdownDescription: "Current version of Git", + Description: "Current version of Git", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "license": schema.ObjectAttribute{ + MarkdownDescription: "Parallels Desktop license", + Computed: true, + AttributeTypes: map[string]attr.Type{ + "state": types.StringType, + "key": types.StringType, + "restricted": types.BoolType, + }, + }, + "api": schema.ObjectAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Service", + Computed: true, + AttributeTypes: map[string]attr.Type{ + "version": types.StringType, + "protocol": types.StringType, + "host": types.StringType, + "port": types.StringType, + "user": types.StringType, + "password": types.StringType, + }, + }, + "installed_dependencies": schema.ListAttribute{ + MarkdownDescription: "List of installed dependencies", + Computed: true, + ElementType: types.StringType, + }, + "install_local": schema.BoolAttribute{ + MarkdownDescription: "Deploy Parallels Desktop in the local machine, this will ignore the need to connect to a remote machine", + Optional: true, + }, + }, +} diff --git a/internal/deploy/resource_schema.go b/internal/deploy/schemas/resource_schema_v2.go similarity index 54% rename from internal/deploy/resource_schema.go rename to internal/deploy/schemas/resource_schema_v2.go index 9871d17..fd2eee8 100644 --- a/internal/deploy/resource_schema.go +++ b/internal/deploy/schemas/resource_schema_v2.go @@ -1,7 +1,8 @@ -package deploy +package schemas import ( "terraform-provider-parallels-desktop/internal/schemas/orchestrator" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" "terraform-provider-parallels-desktop/internal/schemas/sshconnection" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -11,15 +12,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var deployResourceSchemaV0 = schema.Schema{ +var DeployResourceSchemaV2 = schema.Schema{ // This description is used by the documentation generator and the language server. MarkdownDescription: "Parallels Virtual Machine Deployment Resource", Blocks: map[string]schema.Block{ - ApiConfigSchemaName: apiConfigSchemaBlockV0, - sshconnection.SchemaName: sshconnection.SchemaBlock, - orchestrator.SchemaName: orchestrator.SchemaBlock, + ApiConfigSchemaName: ApiConfigSchemaBlockV2, + reverseproxy.SchemaName: reverseproxy.HostBlockV0, + sshconnection.SchemaName: sshconnection.SchemaBlockV0, + orchestrator.SchemaName: orchestrator.SchemaBlockV0, }, - Version: 1, + Version: 2, Attributes: map[string]schema.Attribute{ "current_version": schema.StringAttribute{ MarkdownDescription: "Current version of Parallels Desktop", @@ -61,6 +63,14 @@ var deployResourceSchemaV0 = schema.Schema{ "restricted": types.BoolType, }, }, + "external_ip": schema.StringAttribute{ + MarkdownDescription: "External IP address", + Description: "External IP address", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, "api": schema.ObjectAttribute{ MarkdownDescription: "Parallels Desktop DevOps Service", Computed: true, @@ -82,79 +92,17 @@ var deployResourceSchemaV0 = schema.Schema{ MarkdownDescription: "Deploy Parallels Desktop in the local machine, this will ignore the need to connect to a remote machine", Optional: true, }, - }, -} - -var deployResourceSchemaV1 = schema.Schema{ - // This description is used by the documentation generator and the language server. - MarkdownDescription: "Parallels Virtual Machine Deployment Resource", - Blocks: map[string]schema.Block{ - ApiConfigSchemaName: apiConfigSchemaBlockV1, - sshconnection.SchemaName: sshconnection.SchemaBlock, - orchestrator.SchemaName: orchestrator.SchemaBlock, - }, - Version: 1, - Attributes: map[string]schema.Attribute{ - "current_version": schema.StringAttribute{ - MarkdownDescription: "Current version of Parallels Desktop", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "current_packer_version": schema.StringAttribute{ - MarkdownDescription: "Current version of Hashicorp Packer", - Description: "Current version of Hashicorp Packer", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "current_vagrant_version": schema.StringAttribute{ - MarkdownDescription: "Current version of Hashicorp Vagrant", - Description: "Current version of Hashicorp Vagrant", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "current_git_version": schema.StringAttribute{ - MarkdownDescription: "Current version of Git", - Description: "Current version of Git", + "is_registered_in_orchestrator": schema.BoolAttribute{ + MarkdownDescription: "Is this host registered in the orchestrator", Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "license": schema.ObjectAttribute{ - MarkdownDescription: "Parallels Desktop license", - Computed: true, - AttributeTypes: map[string]attr.Type{ - "state": types.StringType, - "key": types.StringType, - "restricted": types.BoolType, - }, }, - "api": schema.ObjectAttribute{ - MarkdownDescription: "Parallels Desktop DevOps Service", + "orchestrator_host": schema.StringAttribute{ + MarkdownDescription: "Orchestrator host ID", Computed: true, - AttributeTypes: map[string]attr.Type{ - "version": types.StringType, - "protocol": types.StringType, - "host": types.StringType, - "port": types.StringType, - "user": types.StringType, - "password": types.StringType, - }, }, - "installed_dependencies": schema.ListAttribute{ - MarkdownDescription: "List of installed dependencies", + "orchestrator_host_id": schema.StringAttribute{ + MarkdownDescription: "Orchestrator host ID", Computed: true, - ElementType: types.StringType, - }, - "install_local": schema.BoolAttribute{ - MarkdownDescription: "Deploy Parallels Desktop in the local machine, this will ignore the need to connect to a remote machine", - Optional: true, }, }, } diff --git a/internal/deploy/schemas/schema_names.go b/internal/deploy/schemas/schema_names.go new file mode 100644 index 0000000..973708b --- /dev/null +++ b/internal/deploy/schemas/schema_names.go @@ -0,0 +1,3 @@ +package schemas + +var ApiConfigSchemaName = "api_config" diff --git a/internal/helpers/client.go b/internal/helpers/client.go index ef4883a..036a54b 100644 --- a/internal/helpers/client.go +++ b/internal/helpers/client.go @@ -12,6 +12,7 @@ import ( "os" "reflect" "strings" + "time" "terraform-provider-parallels-desktop/internal/clientmodels" @@ -95,6 +96,8 @@ func (c *HttpCaller) RequestDataToClient(verb HttpCallerVerb, url string, header Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, + // Set a timeout for the client for 15 seconds to avoid hanging + Timeout: 60 * time.Second, } } var req *http.Request diff --git a/internal/remoteimage/resource_models.go b/internal/remoteimage/models/resource_models_v0.go similarity index 97% rename from internal/remoteimage/resource_models.go rename to internal/remoteimage/models/resource_models_v0.go index 3c29509..ef9dc2b 100644 --- a/internal/remoteimage/resource_models.go +++ b/internal/remoteimage/models/resource_models_v0.go @@ -1,4 +1,4 @@ -package remoteimage +package models import ( "terraform-provider-parallels-desktop/internal/schemas/authenticator" @@ -13,7 +13,7 @@ import ( ) // VirtualMachineStateResourceModel describes the resource data model. -type RemoteVmResourceModel struct { +type RemoteVmResourceModelV0 struct { Authenticator *authenticator.Authentication `tfsdk:"authenticator"` Host types.String `tfsdk:"host"` Orchestrator types.String `tfsdk:"orchestrator"` diff --git a/internal/remoteimage/models/resource_models_v1.go b/internal/remoteimage/models/resource_models_v1.go new file mode 100644 index 0000000..1728218 --- /dev/null +++ b/internal/remoteimage/models/resource_models_v1.go @@ -0,0 +1,44 @@ +package models + +import ( + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/prlctl" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" + "terraform-provider-parallels-desktop/internal/schemas/sharedfolder" + "terraform-provider-parallels-desktop/internal/schemas/vmconfig" + "terraform-provider-parallels-desktop/internal/schemas/vmspecs" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// VirtualMachineStateResourceModel describes the resource data model. +type RemoteVmResourceModelV1 struct { + Authenticator *authenticator.Authentication `tfsdk:"authenticator"` + Host types.String `tfsdk:"host"` + Orchestrator types.String `tfsdk:"orchestrator"` + ID types.String `tfsdk:"id"` + ExternalIp types.String `tfsdk:"external_ip"` + InternalIp types.String `tfsdk:"internal_ip"` + OrchestratorHostId types.String `tfsdk:"orchestrator_host_id"` + OsType types.String `tfsdk:"os_type"` + CatalogId types.String `tfsdk:"catalog_id"` + Version types.String `tfsdk:"version"` + Architecture types.String `tfsdk:"architecture"` + Name types.String `tfsdk:"name"` + Owner types.String `tfsdk:"owner"` + CatalogConnection types.String `tfsdk:"catalog_connection"` + Path types.String `tfsdk:"path"` + Specs *vmspecs.VmSpecs `tfsdk:"specs"` + PostProcessorScripts []*postprocessorscript.PostProcessorScript `tfsdk:"post_processor_script"` + OnDestroyScript []*postprocessorscript.PostProcessorScript `tfsdk:"on_destroy_script"` + SharedFolder []*sharedfolder.SharedFolder `tfsdk:"shared_folder"` + Config *vmconfig.VmConfig `tfsdk:"config"` + PrlCtl []*prlctl.PrlCtlCmd `tfsdk:"prlctl"` + RunAfterCreate types.Bool `tfsdk:"run_after_create"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + ForceChanges types.Bool `tfsdk:"force_changes"` + KeepRunning types.Bool `tfsdk:"keep_running"` + ReverseProxyHosts []*reverseproxy.ReverseProxyHost `tfsdk:"reverse_proxy_host"` +} diff --git a/internal/remoteimage/resource.go b/internal/remoteimage/resource.go index f1d472f..a144522 100644 --- a/internal/remoteimage/resource.go +++ b/internal/remoteimage/resource.go @@ -8,11 +8,15 @@ import ( "terraform-provider-parallels-desktop/internal/apiclient" "terraform-provider-parallels-desktop/internal/apiclient/apimodels" "terraform-provider-parallels-desktop/internal/common" - "terraform-provider-parallels-desktop/internal/models" + common_models "terraform-provider-parallels-desktop/internal/models" + "terraform-provider-parallels-desktop/internal/remoteimage/models" + "terraform-provider-parallels-desktop/internal/remoteimage/schemas" "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" "terraform-provider-parallels-desktop/internal/telemetry" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" @@ -31,7 +35,7 @@ func NewRemoteVmResource() resource.Resource { // RemoteVmResource defines the resource implementation. type RemoteVmResource struct { - provider *models.ParallelsProviderModel + provider *common_models.ParallelsProviderModel } func (r *RemoteVmResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -39,7 +43,7 @@ func (r *RemoteVmResource) Metadata(ctx context.Context, req resource.MetadataRe } func (r *RemoteVmResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = getSchema(ctx) + resp.Schema = schemas.GetRemoteImageSchemaV1(ctx) } func (r *RemoteVmResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -47,11 +51,11 @@ func (r *RemoteVmResource) Configure(ctx context.Context, req resource.Configure return } - data, ok := req.ProviderData.(*models.ParallelsProviderModel) + data, ok := req.ProviderData.(*common_models.ParallelsProviderModel) if !ok { resp.Diagnostics.AddError( "Unexpected Data Source Configure Type", - fmt.Sprintf("Expected *models.ParallelsProviderModel, got: %T. Please report this issue to the provider developers.", req.ProviderData), + fmt.Sprintf("Expected *common_modesl.ParallelsProviderModel, got: %T. Please report this issue to the provider developers.", req.ProviderData), ) return } @@ -60,7 +64,7 @@ func (r *RemoteVmResource) Configure(ctx context.Context, req resource.Configure } func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data RemoteVmResourceModel + var data models.RemoteVmResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -118,8 +122,8 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques return } - catalogManifest, diag := apiclient.GetCatalogManifest(ctx, *catalogHostConfig, data.CatalogId.ValueString(), data.Version.ValueString(), data.Architecture.ValueString()) - if diag.HasError() { + catalogManifest, catalogManifestDiag := apiclient.GetCatalogManifest(ctx, *catalogHostConfig, data.CatalogId.ValueString(), data.Version.ValueString(), data.Architecture.ValueString()) + if catalogManifestDiag.HasError() { resp.Diagnostics.AddError("Catalog Not Found", fmt.Sprintf("Catalog %s was not found on %s", data.CatalogId.ValueString(), catalogHostConfig.Host)) return } @@ -130,9 +134,9 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques } // Checking if the VM already exists in the host - vm, diag := apiclient.GetVms(ctx, hostConfig, "Name", data.Name.String()) - if diag.HasError() { - resp.Diagnostics.Append(diag...) + vm, vmDiag := apiclient.GetVms(ctx, hostConfig, "Name", data.Name.String()) + if vmDiag.HasError() { + resp.Diagnostics.Append(vmDiag...) return } @@ -145,8 +149,8 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques architecture = catalogManifest.Architecture } - if diags := common.CheckIfEnoughSpecs(ctx, hostConfig, data.Specs, architecture); diags.HasError() { - resp.Diagnostics.Append(diags...) + if specsDiag := common.CheckIfEnoughSpecs(ctx, hostConfig, data.Specs, architecture); specsDiag.HasError() { + resp.Diagnostics.Append(specsDiag...) return } } @@ -183,27 +187,29 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques createMachineRequest.Owner = data.Owner.ValueString() } - response, diag := apiclient.CreateVm(ctx, hostConfig, createMachineRequest) - if diag.HasError() { - resp.Diagnostics.Append(diag...) + response, vmDiag := apiclient.CreateVm(ctx, hostConfig, createMachineRequest) + if vmDiag.HasError() { + resp.Diagnostics.Append(vmDiag...) return } data.ID = types.StringValue(response.ID) tflog.Info(ctx, "Created vm with id "+data.ID.ValueString()) - createdVM, diag := apiclient.GetVm(ctx, hostConfig, response.ID) - if diag.HasError() { - resp.Diagnostics.Append(diag...) + createdVM, vmDiag := apiclient.GetVm(ctx, hostConfig, response.ID) + if vmDiag.HasError() { + resp.Diagnostics.Append(vmDiag...) return } + hostConfig.HostId = createdVM.HostId + // stopping the machine as it might need some operations where the machine needs to be stopped // add anything here in sequence that needs to be done before the machine is started // so we do not loose time waiting for the machine to stop - stoppedVm, diag := common.EnsureMachineStopped(ctx, hostConfig, createdVM) - if diag.HasError() { - resp.Diagnostics.Append(diag...) + stoppedVm, vmDiag := common.EnsureMachineStopped(ctx, hostConfig, createdVM) + if vmDiag.HasError() { + resp.Diagnostics.Append(vmDiag...) } // Applying the Specs block @@ -213,6 +219,10 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return @@ -220,56 +230,111 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques } // Configuring the machine if there is any configuration - if diag := common.VmConfigBlockOnCreate(ctx, hostConfig, stoppedVm, data.Config); diag.HasError() { - resp.Diagnostics.Append(diag...) + if vmBlockDiag := common.VmConfigBlockOnCreate(ctx, hostConfig, stoppedVm, data.Config); vmBlockDiag.HasError() { + resp.Diagnostics.Append(vmBlockDiag...) if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return } // Applying any prlctl commands - if diag := common.PrlCtlBlockOnCreate(ctx, hostConfig, stoppedVm, data.PrlCtl); diag.HasError() { - resp.Diagnostics.Append(diag...) + if prlctlDiag := common.PrlCtlBlockOnCreate(ctx, hostConfig, stoppedVm, data.PrlCtl); prlctlDiag.HasError() { + resp.Diagnostics.Append(prlctlDiag...) if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return } // Processing shared folders - if diag := common.SharedFoldersBlockOnCreate(ctx, hostConfig, stoppedVm, data.SharedFolder); diag.HasError() { - resp.Diagnostics.Append(diag...) + if sharedFolderDiag := common.SharedFoldersBlockOnCreate(ctx, hostConfig, stoppedVm, data.SharedFolder); sharedFolderDiag.HasError() { + resp.Diagnostics.Append(sharedFolderDiag...) if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return } // Running the post processor scripts - if diag := common.RunPostProcessorScript(ctx, hostConfig, stoppedVm, data.PostProcessorScripts); diag.HasError() { - resp.Diagnostics.Append(diag...) + if postProcessDiag := common.RunPostProcessorScript(ctx, hostConfig, stoppedVm, data.PostProcessorScripts); postProcessDiag.HasError() { + resp.Diagnostics.Append(postProcessDiag...) if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return } - // Starting the vm if requested - if data.RunAfterCreate.ValueBool() { + if len(data.ReverseProxyHosts) > 0 { + rpHostConfig := hostConfig + rpHostConfig.HostId = stoppedVm.HostId + rpHosts, updateDiag := updateReverseProxyHostsTarget(ctx, &data, rpHostConfig, stoppedVm) + if updateDiag.HasError() { + resp.Diagnostics.Append(updateDiag...) + return + } + + result, createDiag := reverseproxy.Create(ctx, rpHostConfig, rpHosts) + if createDiag.HasError() { + resp.Diagnostics.Append(createDiag...) + + if diag := reverseproxy.Delete(ctx, rpHostConfig, rpHosts); diag.HasError() { + tflog.Error(ctx, "Error deleting reverse proxy hosts") + } + + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, rpHostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, rpHostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + + apiclient.DeleteVm(ctx, rpHostConfig, data.ID.ValueString()) + } + return + } + + for i := range result { + data.ReverseProxyHosts[i].ID = result[i].ID + } + } + + // Starting the vm by default, otherwise we will stop the VM from being created + if data.RunAfterCreate.ValueBool() || data.KeepRunning.ValueBool() || (data.RunAfterCreate.IsUnknown() && data.KeepRunning.IsUnknown()) { if _, diag := common.EnsureMachineRunning(ctx, hostConfig, stoppedVm); diag.HasError() { resp.Diagnostics.Append(diag...) if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return @@ -280,9 +345,38 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques resp.Diagnostics.Append(diag...) return } + } else { + // If we are not starting the machine, we will stop it + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } + } + + externalIp := "" + internalIp := "" + refreshVm, refreshDiag := apiclient.GetVm(ctx, hostConfig, response.ID) + if refreshDiag.HasError() { + resp.Diagnostics.Append(refreshDiag...) + return + } else { + externalIp = refreshVm.HostExternalIpAddress + internalIp = refreshVm.InternalIpAddress } data.OsType = types.StringValue(createdVM.OS) + data.ExternalIp = types.StringValue(externalIp) + data.InternalIp = types.StringValue(internalIp) + data.OrchestratorHostId = types.StringValue(refreshVm.HostId) if data.OnDestroyScript != nil { for _, script := range data.OnDestroyScript { elements := make([]attr.Value, 0) @@ -309,11 +403,23 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques } } + // Setting the state of the vm to running if that is required + if (data.RunAfterCreate.ValueBool() || data.RunAfterCreate.IsUnknown() || data.RunAfterCreate.IsNull()) && (refreshVm.State == "stopped") { + if _, diag := common.EnsureMachineRunning(ctx, hostConfig, refreshVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) if resp.Diagnostics.HasError() { if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return @@ -321,7 +427,7 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques } func (r *RemoteVmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data RemoteVmResourceModel + var data models.RemoteVmResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -393,8 +499,8 @@ func (r *RemoteVmResource) Read(ctx context.Context, req resource.ReadRequest, r } func (r *RemoteVmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data RemoteVmResourceModel - var currentData RemoteVmResourceModel + var data models.RemoteVmResourceModelV1 + var currentData models.RemoteVmResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -456,6 +562,8 @@ func (r *RemoteVmResource) Update(ctx context.Context, req resource.UpdateReques return } + hostConfig.HostId = vm.HostId + nameChanges := apimodels.NewVmConfigRequest(vm.User) currentState := vm.State needsRestart := false @@ -505,6 +613,12 @@ func (r *RemoteVmResource) Update(ctx context.Context, req resource.UpdateReques // Applying the Specs block if specsChanges { + // We need to stop the machine if it is not stopped + if vm.State != "stopped" && !data.ForceChanges.ValueBool() { + resp.Diagnostics.AddError("vm must be stopped before updating", "Virtual Machine "+vm.Name+" must be stopped before updating, currently "+vm.State) + return + } + if diags := common.SpecsBlockOnUpdate(ctx, hostConfig, vm, data.Specs, currentData.Specs); diags.HasError() { resp.Diagnostics.Append(diags...) return @@ -553,8 +667,84 @@ func (r *RemoteVmResource) Update(ctx context.Context, req resource.UpdateReques data.PostProcessorScripts = currentData.PostProcessorScripts } + if reverseproxy.ReverseProxyHostsDiff(data.ReverseProxyHosts, currentData.ReverseProxyHosts) { + copyCurrentRpHosts := reverseproxy.CopyReverseProxyHosts(currentData.ReverseProxyHosts) + copyRpHosts := reverseproxy.CopyReverseProxyHosts(data.ReverseProxyHosts) + + results, updateDiag := reverseproxy.Update(ctx, hostConfig, copyCurrentRpHosts, copyRpHosts) + if updateDiag.HasError() { + resp.Diagnostics.Append(updateDiag...) + revertResults, _ := reverseproxy.Revert(ctx, hostConfig, copyCurrentRpHosts, copyRpHosts) + for i := range revertResults { + data.ReverseProxyHosts[i].ID = revertResults[i].ID + } + return + } + + for i := range results { + data.ReverseProxyHosts[i].ID = results[i].ID + } + } else { + for i := range currentData.ReverseProxyHosts { + data.ReverseProxyHosts[i].ID = currentData.ReverseProxyHosts[i].ID + } + } + + // Starting the vm by default, otherwise we will stop the VM from being created + if data.RunAfterCreate.ValueBool() || data.KeepRunning.ValueBool() || (data.RunAfterCreate.IsUnknown() && data.KeepRunning.IsUnknown()) { + if _, diag := common.EnsureMachineRunning(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } + + _, diag := apiclient.GetVm(ctx, hostConfig, vm.ID) + if diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + } else { + // If we are not starting the machine, we will stop it + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } + } + + externalIp := "" + internalIp := "" + refreshVm, refreshDiag := apiclient.GetVm(ctx, hostConfig, vm.ID) + if refreshDiag.HasError() { + resp.Diagnostics.Append(refreshDiag...) + return + } else { + externalIp = refreshVm.HostExternalIpAddress + internalIp = refreshVm.InternalIpAddress + } + data.ID = types.StringValue(vm.ID) - data.OsType = types.StringValue(vm.OS) + data.OsType = types.StringValue(refreshVm.OS) + data.ExternalIp = types.StringValue(externalIp) + data.InternalIp = types.StringValue(internalIp) + data.OrchestratorHostId = types.StringValue(refreshVm.HostId) + if data.OnDestroyScript != nil { for _, script := range data.OnDestroyScript { elements := make([]attr.Value, 0) @@ -581,6 +771,13 @@ func (r *RemoteVmResource) Update(ctx context.Context, req resource.UpdateReques } } + if (data.RunAfterCreate.ValueBool() || data.RunAfterCreate.IsUnknown() || data.RunAfterCreate.IsNull()) && (vm.State == "stopped") { + if _, diag := common.EnsureMachineRunning(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + } + tflog.Info(ctx, "Updated vm with id "+data.ID.ValueString()+" and name "+data.Name.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -590,7 +787,7 @@ func (r *RemoteVmResource) Update(ctx context.Context, req resource.UpdateReques } func (r *RemoteVmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data RemoteVmResourceModel + var data models.RemoteVmResourceModelV1 // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -653,6 +850,8 @@ func (r *RemoteVmResource) Delete(ctx context.Context, req resource.DeleteReques if vm == nil { resp.Diagnostics.Append(req.State.Set(ctx, &data)...) return + } else { + hostConfig.HostId = vm.HostId } // Running cleanup script if any @@ -693,6 +892,16 @@ func (r *RemoteVmResource) Delete(ctx context.Context, req resource.DeleteReques time.Sleep(10 * time.Second) } + if len(data.ReverseProxyHosts) > 0 { + rpHostConfig := hostConfig + rpHostConfig.HostId = vm.HostId + rpHostsCopy := reverseproxy.CopyReverseProxyHosts(data.ReverseProxyHosts) + if diag := reverseproxy.Delete(ctx, rpHostConfig, rpHostsCopy); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + } + resp.Diagnostics.Append(req.State.Set(ctx, &data)...) if resp.Diagnostics.HasError() { @@ -703,3 +912,123 @@ func (r *RemoteVmResource) Delete(ctx context.Context, req resource.DeleteReques func (r *RemoteVmResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +func (r *RemoteVmResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { + v0Schema := schemas.GetRemoteImageSchemaV0(ctx) + return map[int64]resource.StateUpgrader{ + 0: { + PriorSchema: &v0Schema, + StateUpgrader: UpgradeStateToV1, + }, + } +} + +func UpgradeStateToV1(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData models.RemoteVmResourceModelV0 + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := models.RemoteVmResourceModelV1{ + Authenticator: priorStateData.Authenticator, + Host: priorStateData.Host, + Orchestrator: priorStateData.Orchestrator, + ID: priorStateData.ID, + OsType: priorStateData.OsType, + ExternalIp: types.StringUnknown(), + InternalIp: types.StringUnknown(), + OrchestratorHostId: types.StringUnknown(), + CatalogId: priorStateData.CatalogId, + Version: priorStateData.Version, + Architecture: priorStateData.Architecture, + Name: priorStateData.Name, + Owner: priorStateData.Owner, + CatalogConnection: priorStateData.CatalogConnection, + Path: priorStateData.Path, + Specs: priorStateData.Specs, + PostProcessorScripts: priorStateData.PostProcessorScripts, + OnDestroyScript: priorStateData.OnDestroyScript, + SharedFolder: priorStateData.SharedFolder, + Config: priorStateData.Config, + PrlCtl: priorStateData.PrlCtl, + RunAfterCreate: priorStateData.RunAfterCreate, + Timeouts: priorStateData.Timeouts, + ForceChanges: priorStateData.ForceChanges, + KeepRunning: types.BoolValue(true), + ReverseProxyHosts: make([]*reverseproxy.ReverseProxyHost, 0), + } + + println(fmt.Sprintf("Upgrading state from version %v", upgradedStateData)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &upgradedStateData)...) +} + +func updateReverseProxyHostsTarget(ctx context.Context, data *models.RemoteVmResourceModelV1, hostConfig apiclient.HostConfig, targetVm *apimodels.VirtualMachine) ([]reverseproxy.ReverseProxyHost, diag.Diagnostics) { + resultDiagnostic := diag.Diagnostics{} + var refreshedVm *apimodels.VirtualMachine + var rpDiag diag.Diagnostics + refreshedVm, rpDiag = common.EnsureMachineHasInternalIp(ctx, hostConfig, targetVm) + if rpDiag.HasError() { + resultDiagnostic.Append(rpDiag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, refreshedVm); diag.HasError() { + return nil, diag + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return nil, resultDiagnostic + } + + modifiedHosts := make([]reverseproxy.ReverseProxyHost, len(data.ReverseProxyHosts)) + for i := range data.ReverseProxyHosts { + host := reverseproxy.ReverseProxyHost{} + host.Host = data.ReverseProxyHosts[i].Host + host.Port = data.ReverseProxyHosts[i].Port + internalIp := refreshedVm.InternalIpAddress + emptyString := "" + + if data.ReverseProxyHosts[i].Cors != nil { + host.Cors = &reverseproxy.ReverseProxyCors{} + host.Cors.AllowedOrigins = data.ReverseProxyHosts[i].Cors.AllowedOrigins + host.Cors.AllowedMethods = data.ReverseProxyHosts[i].Cors.AllowedMethods + host.Cors.AllowedHeaders = data.ReverseProxyHosts[i].Cors.AllowedHeaders + host.Cors.Enabled = data.ReverseProxyHosts[i].Cors.Enabled + } + if data.ReverseProxyHosts[i].Tls != nil { + host.Tls = &reverseproxy.ReverseProxyTls{} + host.Tls.Certificate = data.ReverseProxyHosts[i].Tls.Certificate + host.Tls.PrivateKey = data.ReverseProxyHosts[i].Tls.PrivateKey + host.Tls.Enabled = data.ReverseProxyHosts[i].Tls.Enabled + } + if data.ReverseProxyHosts[i].TcpRoute != nil { + host.TcpRoute = &reverseproxy.ReverseProxyHostTcpRoute{} + host.TcpRoute.TargetPort = data.ReverseProxyHosts[i].TcpRoute.TargetPort + host.TcpRoute.TargetHost = types.StringValue(internalIp) + host.TcpRoute.TargetVmId = types.StringValue(emptyString) + } + + if len(data.ReverseProxyHosts[i].HttpRoute) > 0 { + host.HttpRoute = make([]*reverseproxy.ReverseProxyHttpRoute, len(data.ReverseProxyHosts[i].HttpRoute)) + for j := range modifiedHosts[i].HttpRoute { + httpRoute := reverseproxy.ReverseProxyHttpRoute{} + httpRoute.Path = data.ReverseProxyHosts[i].HttpRoute[j].Path + httpRoute.TargetHost = types.StringValue(internalIp) + httpRoute.TargetPort = data.ReverseProxyHosts[i].HttpRoute[j].TargetPort + httpRoute.TargetVmId = types.StringValue(emptyString) + httpRoute.Pattern = data.ReverseProxyHosts[i].HttpRoute[j].Pattern + httpRoute.Schema = data.ReverseProxyHosts[i].HttpRoute[j].Schema + httpRoute.RequestHeaders = data.ReverseProxyHosts[i].HttpRoute[j].RequestHeaders + httpRoute.ResponseHeaders = data.ReverseProxyHosts[i].HttpRoute[j].ResponseHeaders + host.HttpRoute[j] = &httpRoute + } + } + + modifiedHosts[i] = host + } + + return modifiedHosts, resultDiagnostic +} diff --git a/internal/remoteimage/resource_schema.go b/internal/remoteimage/schemas/resource_schema_v0.go similarity index 97% rename from internal/remoteimage/resource_schema.go rename to internal/remoteimage/schemas/resource_schema_v0.go index 53926a0..5450090 100644 --- a/internal/remoteimage/resource_schema.go +++ b/internal/remoteimage/schemas/resource_schema_v0.go @@ -1,4 +1,4 @@ -package remoteimage +package schemas import ( "context" @@ -19,7 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) -func getSchema(ctx context.Context) schema.Schema { +func GetRemoteImageSchemaV0(ctx context.Context) schema.Schema { return schema.Schema{ // This description is used by the documentation generator and the language server. MarkdownDescription: "Parallels Virtual Machine State Resource", @@ -32,6 +32,7 @@ func getSchema(ctx context.Context) schema.Schema { vmconfig.SchemaName: vmconfig.SchemaBlock, prlctl.SchemaName: prlctl.SchemaBlock, }, + Version: 0, Attributes: map[string]schema.Attribute{ "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ Create: true, diff --git a/internal/remoteimage/schemas/resource_schema_v1.go b/internal/remoteimage/schemas/resource_schema_v1.go new file mode 100644 index 0000000..7429608 --- /dev/null +++ b/internal/remoteimage/schemas/resource_schema_v1.go @@ -0,0 +1,146 @@ +package schemas + +import ( + "context" + + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/prlctl" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" + "terraform-provider-parallels-desktop/internal/schemas/sharedfolder" + "terraform-provider-parallels-desktop/internal/schemas/vmconfig" + "terraform-provider-parallels-desktop/internal/schemas/vmspecs" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func GetRemoteImageSchemaV1(ctx context.Context) schema.Schema { + return schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Parallels Virtual Machine State Resource", + Blocks: map[string]schema.Block{ + authenticator.SchemaName: authenticator.SchemaBlock, + vmspecs.SchemaName: vmspecs.SchemaBlock, + postprocessorscript.SchemaName: postprocessorscript.SchemaBlock, + "on_destroy_script": postprocessorscript.SchemaBlock, + sharedfolder.SchemaName: sharedfolder.SchemaBlock, + vmconfig.SchemaName: vmconfig.SchemaBlock, + prlctl.SchemaName: prlctl.SchemaBlock, + reverseproxy.SchemaName: reverseproxy.HostBlockV0, + }, + Version: 1, + Attributes: map[string]schema.Attribute{ + "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ + Create: true, + }), + "force_changes": schema.BoolAttribute{ + MarkdownDescription: "Force changes, this will force the VM to be stopped and started again", + Optional: true, + }, + "host": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Host", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + }, + "orchestrator": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Orchestrator", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine Id", + Computed: true, + }, + "orchestrator_host_id": schema.StringAttribute{ + MarkdownDescription: "Orchestrator Host Id if the VM is running in an orchestrator", + Computed: true, + }, + "os_type": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine OS type", + Computed: true, + }, + "catalog_id": schema.StringAttribute{ + MarkdownDescription: "Catalog Id to pull", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "version": schema.StringAttribute{ + MarkdownDescription: "Catalog version to pull, if empty will pull the 'latest' version", + Optional: true, + }, + "architecture": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine architecture", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine name to create, this needs to be unique in the host", + Required: true, + }, + "owner": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine owner", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "catalog_connection": schema.StringAttribute{ + MarkdownDescription: "Parallels DevOps Catalog Connection", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "path": schema.StringAttribute{ + MarkdownDescription: "Path", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "run_after_create": schema.BoolAttribute{ + MarkdownDescription: "Run after create, this will make the VM to run after creation", + Optional: true, + DeprecationMessage: "Use the `keep_running` attribute instead", + }, + "external_ip": schema.StringAttribute{ + MarkdownDescription: "VM external IP address", + Computed: true, + }, + "internal_ip": schema.StringAttribute{ + MarkdownDescription: "VM internal IP address", + Computed: true, + }, + "keep_running": schema.BoolAttribute{ + MarkdownDescription: "This will keep the VM running after the terraform apply", + Optional: true, + }, + }, + } +} diff --git a/internal/schemas/orchestrator/main.go b/internal/schemas/orchestrator/main.go index 01d3922..96f8657 100644 --- a/internal/schemas/orchestrator/main.go +++ b/internal/schemas/orchestrator/main.go @@ -2,6 +2,7 @@ package orchestrator import ( "context" + "strings" "terraform-provider-parallels-desktop/internal/apiclient" "terraform-provider-parallels-desktop/internal/apiclient/apimodels" @@ -75,8 +76,11 @@ func RegisterWithHost(context context.Context, plan OrchestratorRegistration, di return "", diagnostics } -func IsAlreadyRegistered(context context.Context, data OrchestratorRegistration, disableTlsValidation bool) (bool, diag.Diagnostics) { +func IsAlreadyRegistered(context context.Context, data OrchestratorRegistration, disableTlsValidation bool) (bool, *apimodels.OrchestratorHost, diag.Diagnostics) { diagnostics := diag.Diagnostics{} + if data.Orchestrator == nil { + return false, nil, diagnostics + } hostConfig := apiclient.HostConfig{ Host: data.Orchestrator.GetHost(), @@ -90,16 +94,25 @@ func IsAlreadyRegistered(context context.Context, data OrchestratorRegistration, currentHostId := data.HostId.ValueString() currentHostUrl := helpers.GetHostApiBaseUrl(data.GetHost()) - response, _ := apiclient.GetOrchestratorHost(context, hostConfig, data.HostId.ValueString()) + currentHostDescription := data.Description.ValueString() + response, _ := apiclient.GetOrchestratorHosts(context, hostConfig) if response == nil { - return false, diagnostics + return false, nil, diagnostics } - if currentHostId == response.ID || currentHostUrl == response.Host { - return true, diagnostics + if len(response) == 0 { + return false, nil, diagnostics + } + + for _, host := range response { + if strings.EqualFold(currentHostId, host.ID) || + strings.EqualFold(currentHostUrl, host.Host) || + strings.EqualFold(currentHostDescription, host.Description) { + return true, &host, diagnostics + } } - return false, diagnostics + return false, nil, diagnostics } func UnregisterWithHost(context context.Context, data OrchestratorRegistration, disableTlsValidation bool) diag.Diagnostics { diff --git a/internal/schemas/orchestrator/schema.go b/internal/schemas/orchestrator/schema.go index d012fb9..bc5c088 100644 --- a/internal/schemas/orchestrator/schema.go +++ b/internal/schemas/orchestrator/schema.go @@ -9,41 +9,43 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var SchemaName = "orchestrator_registration" -var SchemaBlock = schema.SingleNestedBlock{ - MarkdownDescription: "Orchestrator connection details", - Blocks: map[string]schema.Block{ - "host_credentials": authenticator.SchemaBlock, - OrchestratorSchemaName: OrchestratorSchemaBlock, - }, - Attributes: map[string]schema.Attribute{ - "schema": schema.StringAttribute{ - MarkdownDescription: "Host Schema", - Optional: true, +var ( + SchemaName = "orchestrator_registration" + SchemaBlockV0 = schema.SingleNestedBlock{ + MarkdownDescription: "Orchestrator connection details", + Blocks: map[string]schema.Block{ + "host_credentials": authenticator.SchemaBlock, + OrchestratorSchemaName: OrchestratorSchemaBlock, }, - "host": schema.StringAttribute{ - MarkdownDescription: "Host address", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), + Attributes: map[string]schema.Attribute{ + "schema": schema.StringAttribute{ + MarkdownDescription: "Host Schema", + Optional: true, + }, + "host": schema.StringAttribute{ + MarkdownDescription: "Host address", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "port": schema.StringAttribute{ + MarkdownDescription: "Host port", + Optional: true, + }, + "tags": schema.ListAttribute{ + MarkdownDescription: "Host tags", + Optional: true, + ElementType: types.StringType, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "Host description", + Optional: true, + }, + "host_id": schema.StringAttribute{ + MarkdownDescription: "Host Orchestrator ID", + Computed: true, }, }, - "port": schema.StringAttribute{ - MarkdownDescription: "Host port", - Optional: true, - }, - "tags": schema.ListAttribute{ - MarkdownDescription: "Host tags", - Optional: true, - ElementType: types.StringType, - }, - "description": schema.StringAttribute{ - MarkdownDescription: "Host description", - Optional: true, - }, - "host_id": schema.StringAttribute{ - MarkdownDescription: "Host Orchestrator ID", - Computed: true, - }, - }, -} + } +) diff --git a/internal/schemas/reverseproxy/common.go b/internal/schemas/reverseproxy/common.go new file mode 100644 index 0000000..5188c95 --- /dev/null +++ b/internal/schemas/reverseproxy/common.go @@ -0,0 +1,3 @@ +package reverseproxy + +var SchemaName = "reverse_proxy_host" diff --git a/internal/schemas/reverseproxy/cors_schema_v0.go b/internal/schemas/reverseproxy/cors_schema_v0.go new file mode 100644 index 0000000..f50f6be --- /dev/null +++ b/internal/schemas/reverseproxy/cors_schema_v0.go @@ -0,0 +1,48 @@ +package reverseproxy + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var CorsSchemaBlockV0 = schema.SingleNestedBlock{ + MarkdownDescription: "Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration", + Description: "Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration", + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable CORS", + Description: "Enable CORS", + Optional: true, + }, + "allowed_origins": schema.ListAttribute{ + MarkdownDescription: "Allowed origins", + Description: "Allowed origins", + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "allowed_methods": schema.ListAttribute{ + MarkdownDescription: "Allowed methods", + Description: "Allowed methods", + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "allowed_headers": schema.ListAttribute{ + MarkdownDescription: "Allowed headers", + Description: "Allowed headers", + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + }, +} diff --git a/internal/schemas/reverseproxy/host_schema_v0.go b/internal/schemas/reverseproxy/host_schema_v0.go new file mode 100644 index 0000000..051ea33 --- /dev/null +++ b/internal/schemas/reverseproxy/host_schema_v0.go @@ -0,0 +1,33 @@ +package reverseproxy + +import "github.com/hashicorp/terraform-plugin-framework/resource/schema" + +var HostBlockV0 = schema.ListNestedBlock{ + MarkdownDescription: "Parallels Desktop DevOps Reverse Proxy configuration", + Description: "Parallels Desktop DevOps Reverse Proxy configuration", + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "tcp_route": TcpRouteSchemaBlockV0, + "cors": CorsSchemaBlockV0, + "tls": TlsSchemaBlockV0, + "http_routes": HttpRouteSchemaBlockV0, + }, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy Host id", + Description: "Reverse proxy Host id", + Computed: true, + }, + "host": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy host", + Description: "Reverse proxy host", + Optional: true, + }, + "port": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy port", + Description: "Reverse proxy port", + Required: true, + }, + }, + }, +} diff --git a/internal/schemas/reverseproxy/http_route_schema_v0.go b/internal/schemas/reverseproxy/http_route_schema_v0.go new file mode 100644 index 0000000..5f48230 --- /dev/null +++ b/internal/schemas/reverseproxy/http_route_schema_v0.go @@ -0,0 +1,84 @@ +package reverseproxy + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var HttpRouteSchemaBlockV0 = schema.ListNestedBlock{ + MarkdownDescription: "Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration", + Description: "Parallels Desktop DevOps Reverse Proxy Http Route CORS configuration", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "path": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy HTTP Route path", + Description: "Reverse proxy HTTP Route path", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRelative().AtName("path").AtParent(), + path.MatchRelative().AtName("pattern").AtParent(), + }...), + }, + }, + "target_host": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy HTTP Route target host", + Description: "Reverse proxy HTTP Route target host", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRelative().AtName("target_host").AtParent(), + path.MatchRelative().AtName("target_vm_id").AtParent(), + }...), + }, + }, + "target_vm_id": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy HTTP Route target VM id", + Description: "Reverse proxy HTTP Route target VM id", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRelative().AtName("target_host").AtParent(), + path.MatchRelative().AtName("target_vm_id").AtParent(), + }...), + }, + }, + "target_port": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy HTTP Route target port", + Description: "Reverse proxy HTTP Route target port", + Optional: true, + }, + "schema": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy HTTP Route schema", + Description: "Reverse proxy HTTP Route schema", + Optional: true, + }, + "pattern": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy HTTP Route pattern", + Description: "Reverse proxy HTTP Route pattern", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRelative().AtName("path").AtParent(), + path.MatchRelative().AtName("pattern").AtParent(), + }...), + }, + }, + "request_headers": schema.MapAttribute{ + MarkdownDescription: "Reverse proxy HTTP Route request headers", + Description: "Reverse proxy HTTP Route request headers", + Optional: true, + ElementType: types.StringType, + }, + "response_headers": schema.MapAttribute{ + MarkdownDescription: "Reverse proxy HTTP Route response headers", + Description: "Reverse proxy HTTP Route response headers", + Optional: true, + ElementType: types.StringType, + }, + }, + }, +} diff --git a/internal/schemas/reverseproxy/models.go b/internal/schemas/reverseproxy/models.go new file mode 100644 index 0000000..c63cd74 --- /dev/null +++ b/internal/schemas/reverseproxy/models.go @@ -0,0 +1,372 @@ +package reverseproxy + +import ( + "terraform-provider-parallels-desktop/internal/common" + + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +type ReverseProxyHost struct { + Index int `tfsdk:"-"` + ID basetypes.StringValue `tfsdk:"id"` + Host basetypes.StringValue `tfsdk:"host"` + Port string `tfsdk:"port"` + Cors *ReverseProxyCors `tfsdk:"cors"` + Tls *ReverseProxyTls `tfsdk:"tls"` + HttpRoute []*ReverseProxyHttpRoute `tfsdk:"http_routes"` + TcpRoute *ReverseProxyHostTcpRoute `tfsdk:"tcp_route"` +} + +func (o *ReverseProxyHost) Copy() ReverseProxyHost { + result := ReverseProxyHost{} + if o == nil { + return result + } + + result.ID = o.ID + result.Host = o.Host + result.Port = o.Port + if o.Cors != nil { + cors := o.Cors.Copy() + result.Cors = &cors + } + if o.Tls != nil { + tls := o.Tls.Copy() + result.Tls = &tls + } + if len(o.HttpRoute) > 0 { + result.HttpRoute = make([]*ReverseProxyHttpRoute, len(o.HttpRoute)) + for i, v := range o.HttpRoute { + route := v.Copy() + result.HttpRoute[i] = &route + } + } + if o.TcpRoute != nil { + tcpRoute := o.TcpRoute.Copy() + result.TcpRoute = &tcpRoute + } + + return result +} + +func (o *ReverseProxyHost) GetHost() string { + if o == nil { + return "" + } + + host := common.GetString(o.Host) + if host == "" { + host = allAddress + } + + if o.Port != "" { + return host + ":" + o.Port + } + + return host +} + +func (o *ReverseProxyHost) Diff(other *ReverseProxyHost) bool { + if o == nil && other == nil { + return false + } + if o == nil && other != nil { + return true + } + if o != nil && other == nil { + return true + } + if common.GetString(o.ID) != common.GetString(other.ID) { + return true + } + if common.GetString(o.Host) != common.GetString(other.Host) { + return true + } + if o.Port != other.Port { + return true + } + if o.Cors.Diff(other.Cors) { + return true + } + if o.Tls.Diff(other.Tls) { + return true + } + if len(o.HttpRoute) != len(other.HttpRoute) { + return true + } + for i, v := range o.HttpRoute { + if v.Diff(other.HttpRoute[i]) { + return true + } + } + + return o.TcpRoute.Diff(other.TcpRoute) +} + +type ReverseProxyHostTcpRoute struct { + TargetPort basetypes.StringValue `tfsdk:"target_port"` + TargetHost basetypes.StringValue `tfsdk:"target_host"` + TargetVmId basetypes.StringValue `tfsdk:"target_vm_id"` +} + +func (o *ReverseProxyHostTcpRoute) GetHost() string { + host := common.GetString(o.TargetHost) + port := common.GetString(o.TargetPort) + vmId := common.GetString(o.TargetVmId) + + if host == "" && vmId == "" { + host = allAddress + } + if host == "" && vmId != "" { + host = vmId + } + + if port != "" { + host += ":" + port + } + + return host +} + +func (o *ReverseProxyHostTcpRoute) Copy() ReverseProxyHostTcpRoute { + result := ReverseProxyHostTcpRoute{} + if o == nil { + return result + } + + result.TargetPort = o.TargetPort + result.TargetHost = o.TargetHost + result.TargetVmId = o.TargetVmId + + return result +} + +func (o *ReverseProxyHostTcpRoute) Diff(other *ReverseProxyHostTcpRoute) bool { + if o == nil && other == nil { + return false + } + if o == nil && other != nil { + return true + } + if o != nil && other == nil { + return true + } + if common.GetString(o.TargetPort) != common.GetString(other.TargetPort) { + return true + } + if common.GetString(o.TargetHost) != common.GetString(other.TargetHost) { + return true + } + if common.GetString(o.TargetVmId) != common.GetString(other.TargetVmId) { + return true + } + return false +} + +type ReverseProxyCors struct { + Enabled bool `tfsdk:"enabled"` + AllowedOrigins []string `tfsdk:"allowed_origins"` + AllowedMethods []string `tfsdk:"allowed_methods"` + AllowedHeaders []string `tfsdk:"allowed_headers"` +} + +func (o *ReverseProxyCors) Copy() ReverseProxyCors { + if o == nil { + return ReverseProxyCors{} + } + c := *o + return c +} + +func (o *ReverseProxyCors) Diff(other *ReverseProxyCors) bool { + if o == nil && other == nil { + return false + } + if o == nil && other != nil { + return true + } + if o != nil && other == nil { + return true + } + if o.Enabled != other.Enabled { + return true + } + if len(o.AllowedOrigins) != len(other.AllowedOrigins) { + return true + } + for i, v := range o.AllowedOrigins { + if other.AllowedOrigins[i] != v { + return true + } + } + if len(o.AllowedMethods) != len(other.AllowedMethods) { + return true + } + for i, v := range o.AllowedMethods { + if other.AllowedMethods[i] != v { + return true + } + } + if len(o.AllowedHeaders) != len(other.AllowedHeaders) { + return true + } + for i, v := range o.AllowedHeaders { + if other.AllowedHeaders[i] != v { + return true + } + } + return false +} + +type ReverseProxyTls struct { + Enabled bool `tfsdk:"enabled"` + Certificate string `tfsdk:"certificate"` + PrivateKey string `tfsdk:"private_key"` +} + +func (o *ReverseProxyTls) Copy() ReverseProxyTls { + if o == nil { + return ReverseProxyTls{} + } + c := *o + return c +} + +func (o *ReverseProxyTls) Diff(other *ReverseProxyTls) bool { + if o == nil && other == nil { + return false + } + if o == nil && other != nil { + return true + } + if o != nil && other == nil { + return true + } + if o.Enabled != other.Enabled { + return true + } + if o.Certificate != other.Certificate { + return true + } + if o.PrivateKey != other.PrivateKey { + return true + } + return false +} + +type ReverseProxyHttpRoute struct { + TargetPort basetypes.StringValue `tfsdk:"target_port"` + TargetHost basetypes.StringValue `tfsdk:"target_host"` + TargetVmId basetypes.StringValue `tfsdk:"target_vm_id"` + Path string `tfsdk:"path"` + Pattern string `tfsdk:"pattern"` + Schema string `tfsdk:"schema"` + RequestHeaders map[string]string `tfsdk:"request_headers"` + ResponseHeaders map[string]string `tfsdk:"response_headers"` +} + +func (o *ReverseProxyHttpRoute) GetHost() string { + host := common.GetString(o.TargetHost) + port := common.GetString(o.TargetPort) + vmId := common.GetString(o.TargetVmId) + + if host == "" && vmId == "" { + host = allAddress + } + if host == "" && vmId != "" { + host = vmId + } + + if port != "" { + host += ":" + port + } + + if o.Schema != "" { + host = o.Schema + "://" + host + } + return host +} + +func (o *ReverseProxyHttpRoute) Copy() ReverseProxyHttpRoute { + result := ReverseProxyHttpRoute{} + result.Path = o.Path + result.Pattern = o.Pattern + result.TargetHost = o.TargetHost + result.TargetPort = o.TargetPort + result.TargetVmId = o.TargetVmId + result.Schema = o.Schema + result.RequestHeaders = o.RequestHeaders + result.ResponseHeaders = o.ResponseHeaders + + return result +} + +func (o *ReverseProxyHttpRoute) Diff(other *ReverseProxyHttpRoute) bool { + if o == nil && other == nil { + return false + } + if o == nil && other != nil { + return true + } + if o != nil && other == nil { + return true + } + if o.Path != other.Path { + return true + } + if o.Pattern != other.Pattern { + return true + } + if common.GetString(o.TargetHost) != common.GetString(other.TargetHost) { + return true + } + if common.GetString(o.TargetPort) != common.GetString(other.TargetPort) { + return true + } + if common.GetString(o.TargetVmId) != common.GetString(other.TargetVmId) { + return true + } + if o.Schema != other.Schema { + return true + } + if len(o.RequestHeaders) != len(other.RequestHeaders) { + return true + } + for k, v := range o.RequestHeaders { + if other.RequestHeaders[k] != v { + return true + } + } + if len(o.ResponseHeaders) != len(other.ResponseHeaders) { + return true + } + for k, v := range o.ResponseHeaders { + if other.ResponseHeaders[k] != v { + return true + } + } + return false +} + +func ReverseProxyHostsDiff(a, b []*ReverseProxyHost) bool { + if len(a) != len(b) { + return true + } + for i, v := range a { + if v.Diff(b[i]) { + return true + } + } + return false +} + +func CopyReverseProxyHosts(a []*ReverseProxyHost) []ReverseProxyHost { + if a == nil { + return nil + } + b := make([]ReverseProxyHost, len(a)) + for i, v := range a { + b[i] = v.Copy() + } + return b +} diff --git a/internal/schemas/reverseproxy/operations.go b/internal/schemas/reverseproxy/operations.go new file mode 100644 index 0000000..5f4936b --- /dev/null +++ b/internal/schemas/reverseproxy/operations.go @@ -0,0 +1,341 @@ +package reverseproxy + +import ( + "context" + "strings" + + "terraform-provider-parallels-desktop/internal/apiclient" + "terraform-provider-parallels-desktop/internal/apiclient/apimodels" + "terraform-provider-parallels-desktop/internal/common" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var allAddress = "0.0.0.0" + +func Read() diag.Diagnostics { + diagnostic := diag.Diagnostics{} + return diagnostic +} + +func Create(ctx context.Context, config apiclient.HostConfig, request []ReverseProxyHost) ([]ReverseProxyHost, diag.Diagnostics) { + diagnostic := diag.Diagnostics{} + for _, host := range request { + if h, diag := createHost(ctx, config, host); diag.HasError() { + diagnostic = append(diagnostic, diag...) + } else { + host.ID = h.ID + } + } + + return request, diagnostic +} + +func Revert(ctx context.Context, config apiclient.HostConfig, currentHosts []ReverseProxyHost, requestHosts []ReverseProxyHost) ([]ReverseProxyHost, diag.Diagnostics) { + diagnostic := diag.Diagnostics{} + + // we will delete all the hosts that are in the request as we do not know what was changed + for _, host := range requestHosts { + if exists, _ := apiclient.GetReverseProxyHost(ctx, config, host.GetHost()); exists != nil { + if diag := deleteHost(ctx, config, host); diag.HasError() { + tflog.Error(ctx, "Error deleting host "+host.GetHost()) + } + } + } + + // we will create all the hosts that are in the currentHosts as we do not know what was changed + for i, host := range currentHosts { + if exists, _ := apiclient.GetReverseProxyHost(ctx, config, host.GetHost()); exists == nil { + if r, diag := createHost(ctx, config, host); diag.HasError() { + diagnostic = append(diagnostic, diag...) + } else { + requestHosts[i].ID = r.ID + } + } + } + + return requestHosts, diagnostic +} + +func Update(ctx context.Context, config apiclient.HostConfig, currentHosts []ReverseProxyHost, requestHosts []ReverseProxyHost) ([]ReverseProxyHost, diag.Diagnostics) { + diagnostic := diag.Diagnostics{} + hasChanges := diff(currentHosts, requestHosts) + if !hasChanges { + return nil, diagnostic + } + + // Getting a list of reverse proxy hosts to delete as they exist in the currentHosts and not in the requestHosts + toDelete := getHostsToDelete(currentHosts, requestHosts) + toCreate := getHostsToCreate(currentHosts, requestHosts) + toUpdate := getHostsToUpdate(currentHosts, requestHosts) + + for _, host := range toDelete { + if diag := deleteHost(ctx, config, host); diag.HasError() { + diagnostic = append(diagnostic, diag...) + } + } + + for _, host := range toCreate { + h, createDiag := createHost(ctx, config, host) + if createDiag.HasError() { + diagnostic = append(diagnostic, createDiag...) + } + requestHosts[host.Index].ID = h.ID + } + + for _, host := range toUpdate { + currentHost := currentHosts[host.Index] + h, updateDiag := updateHost(ctx, config, currentHost, host) + if updateDiag.HasError() { + diagnostic = append(diagnostic, updateDiag...) + } + + requestHosts[host.Index].ID = h.ID + } + + return requestHosts, diagnostic +} + +func Delete(ctx context.Context, config apiclient.HostConfig, request []ReverseProxyHost) diag.Diagnostics { + diagnostic := diag.Diagnostics{} + for _, host := range request { + if diag := deleteHost(ctx, config, host); diag.HasError() { + diagnostic = append(diagnostic, diag...) + } + } + return diagnostic +} + +func diff(a, b []ReverseProxyHost) bool { + if len(a) != len(b) { + return true + } + for i, v := range a { + if v.Diff(&b[i]) { + return true + } + } + + return false +} + +func getHostsToDelete(currentHosts, requestHosts []ReverseProxyHost) []ReverseProxyHost { + toDelete := make([]ReverseProxyHost, 0) + if len(currentHosts) > 0 && len(requestHosts) == 0 { + for i, v := range currentHosts { + v.Index = i + toDelete = append(toDelete, v) + } + return toDelete + } + + for i, v := range currentHosts { + found := false + vHost := v.GetHost() + for _, r := range requestHosts { + rHost := r.GetHost() + if strings.EqualFold(vHost, rHost) { + found = true + break + } + } + + if !found { + toDelete = append(toDelete, currentHosts[i]) + } + } + + return toDelete +} + +func getHostsToUpdate(currentHosts, requestHosts []ReverseProxyHost) []ReverseProxyHost { + toUpdate := make([]ReverseProxyHost, 0) + if len(requestHosts) == 0 || len(currentHosts) == 0 { + return toUpdate + } + + for i, v := range requestHosts { + vHost := v.GetHost() + for _, r := range currentHosts { + rHost := r.GetHost() + if strings.EqualFold(vHost, rHost) { + if currentHosts[i].Diff(&v) { + requestHosts[i].Index = i + toUpdate = append(toUpdate, requestHosts[i]) + } + break + } + } + } + + return toUpdate +} + +func getHostsToCreate(currentHosts, requestHosts []ReverseProxyHost) []ReverseProxyHost { + toCreate := make([]ReverseProxyHost, 0) + if len(requestHosts) == 0 { + return toCreate + } + + if len(currentHosts) == 0 { + for i, v := range requestHosts { + v.Index = i + toCreate = append(toCreate, v) + } + return toCreate + } + + for i, v := range requestHosts { + found := false + vHost := v.GetHost() + for _, r := range currentHosts { + rHost := r.GetHost() + if strings.EqualFold(vHost, rHost) { + found = true + break + } + } + + if !found { + requestHosts[i].Index = i + toCreate = append(toCreate, requestHosts[i]) + } + } + + return toCreate +} + +func mapReverseProxyToApiModel(v ReverseProxyHost) apimodels.ReverseProxyHost { + requestHost := apimodels.ReverseProxyHost{ + Port: v.Port, + } + + if common.GetString(v.ID) != "" { + requestHost.ID = common.GetString(v.ID) + } + + if common.GetString(v.Host) == "" { + requestHost.Host = allAddress + } else { + requestHost.Host = common.GetString(v.Host) + } + + if v.Tls != nil { + requestHost.Tls = &apimodels.ReverseProxyHostTls{ + Cert: v.Tls.Certificate, + Key: v.Tls.PrivateKey, + Enabled: v.Tls.Enabled, + } + } + if v.Cors != nil { + requestHost.Cors = &apimodels.ReverseProxyHostCors{ + Enabled: v.Cors.Enabled, + AllowedOrigins: v.Cors.AllowedOrigins, + AllowedMethods: v.Cors.AllowedMethods, + AllowedHeaders: v.Cors.AllowedHeaders, + } + } + if v.TcpRoute != nil { + requestHost.TcpRoute = &apimodels.ReverseProxyHostTcpRoute{} + if common.GetString(v.TcpRoute.TargetHost) != "" { + requestHost.TcpRoute.TargetHost = common.GetString(v.TcpRoute.TargetHost) + } + if common.GetString(v.TcpRoute.TargetPort) != "" { + requestHost.TcpRoute.TargetPort = common.GetString(v.TcpRoute.TargetPort) + } + if common.GetString(v.TcpRoute.TargetVmId) != "" { + requestHost.TcpRoute.TargetVmId = common.GetString(v.TcpRoute.TargetVmId) + } + if common.GetString(v.TcpRoute.TargetHost) == "" && common.GetString(v.TcpRoute.TargetVmId) == "" { + requestHost.TcpRoute.TargetHost = allAddress + } + } + + if len(v.HttpRoute) > 0 { + requestHost.HttpRoutes = make([]*apimodels.ReverseProxyHostHttpRoute, 0) + for _, route := range v.HttpRoute { + httpRoute := apimodels.ReverseProxyHostHttpRoute{ + TargetHost: common.GetString(route.TargetHost), + TargetPort: common.GetString(route.TargetPort), + TargetVmId: common.GetString(route.TargetVmId), + Path: route.Path, + Schema: route.Schema, + Pattern: route.Pattern, + RequestHeaders: route.RequestHeaders, + ResponseHeaders: route.ResponseHeaders, + } + if common.GetString(route.TargetHost) == "" && common.GetString(route.TargetVmId) == "" { + httpRoute.TargetHost = allAddress + } + + requestHost.HttpRoutes = append(requestHost.HttpRoutes, &httpRoute) + } + } + + return requestHost +} + +func deleteHost(ctx context.Context, config apiclient.HostConfig, host ReverseProxyHost) diag.Diagnostics { + diagnostic := diag.Diagnostics{} + + if common.GetString(host.Host) == "" { + host.Host = types.StringValue(allAddress) + } + + if exists, _ := apiclient.GetReverseProxyHost(ctx, config, host.GetHost()); exists != nil { + if diag := apiclient.DeleteReverseProxyHost(ctx, config, host.GetHost()); diag.HasError() { + diagnostic = append(diagnostic, diag...) + } + } + + return diagnostic +} + +func createHost(ctx context.Context, config apiclient.HostConfig, host ReverseProxyHost) (ReverseProxyHost, diag.Diagnostics) { + diagnostic := diag.Diagnostics{} + + // if the host id is not set, we will check if the host exists and delete it + if exists, _ := apiclient.GetReverseProxyHost(ctx, config, host.GetHost()); exists != nil { + if diag := apiclient.DeleteReverseProxyHost(ctx, config, host.GetHost()); diag.HasError() { + diagnostic = append(diagnostic, diag...) + } + } + + r, createDiag := apiclient.CreateReverseProxyHost(ctx, config, mapReverseProxyToApiModel(host)) + if createDiag.HasError() || r == nil { + diagnostic = append(diagnostic, createDiag...) + return host, diagnostic + } + + host.ID = types.StringValue(r.ID) + return host, diagnostic +} + +func updateHost(ctx context.Context, config apiclient.HostConfig, currentHost ReverseProxyHost, requestHost ReverseProxyHost) (ReverseProxyHost, diag.Diagnostics) { + diagnostic := diag.Diagnostics{} + + if exists, _ := apiclient.GetReverseProxyHost(ctx, config, currentHost.GetHost()); exists != nil { + if diag := apiclient.DeleteReverseProxyHost(ctx, config, currentHost.GetHost()); diag.HasError() { + diagnostic = append(diagnostic, diag...) + } + } + + if exists, diag := apiclient.GetReverseProxyHost(ctx, config, requestHost.GetHost()); exists != nil { + if diag.HasError() { + diagnostic = append(diagnostic, diag...) + } + + requestHost.ID = types.StringValue(exists.ID) + return requestHost, diagnostic + } + + r, createDiag := apiclient.CreateReverseProxyHost(ctx, config, mapReverseProxyToApiModel(requestHost)) + if createDiag.HasError() { + diagnostic = append(diagnostic, createDiag...) + } + + requestHost.ID = types.StringValue(r.ID) + return requestHost, diagnostic +} diff --git a/internal/schemas/reverseproxy/tcp_route_schema_v0.go b/internal/schemas/reverseproxy/tcp_route_schema_v0.go new file mode 100644 index 0000000..8b65a5a --- /dev/null +++ b/internal/schemas/reverseproxy/tcp_route_schema_v0.go @@ -0,0 +1,42 @@ +package reverseproxy + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var TcpRouteSchemaBlockV0 = schema.SingleNestedBlock{ + MarkdownDescription: "Parallels Desktop DevOps Reverse Proxy TCP Route configuration", + Description: "Parallels Desktop DevOps Reverse Proxy TCP Route configuration", + Attributes: map[string]schema.Attribute{ + "target_host": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy host", + Description: "Reverse proxy host", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRelative().AtName("target_host").AtParent(), + path.MatchRelative().AtName("target_vm_id").AtParent(), + }...), + }, + }, + "target_port": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy port", + Description: "reverse proxy port", + Optional: true, + }, + "target_vm_id": schema.StringAttribute{ + MarkdownDescription: "Reverse proxy target VM ID", + Description: "Reverse proxy target VM ID", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRelative().AtName("target_host").AtParent(), + path.MatchRelative().AtName("target_vm_id").AtParent(), + }...), + }, + }, + }, +} diff --git a/internal/schemas/reverseproxy/tls_schema_v0.go b/internal/schemas/reverseproxy/tls_schema_v0.go new file mode 100644 index 0000000..e8f84a3 --- /dev/null +++ b/internal/schemas/reverseproxy/tls_schema_v0.go @@ -0,0 +1,27 @@ +package reverseproxy + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +var TlsSchemaBlockV0 = schema.SingleNestedBlock{ + MarkdownDescription: "Parallels Desktop DevOps Reverse Proxy Http Route TLS configuration", + Description: "Parallels Desktop DevOps Reverse Proxy Http Route TLS configuration", + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable TLS", + Description: "Enable TLS", + Optional: true, + }, + "certificate": schema.StringAttribute{ + MarkdownDescription: "TLS Certificate", + Description: "TLS Certificate", + Optional: true, + }, + "private_key": schema.StringAttribute{ + MarkdownDescription: "TLS Private Key", + Description: "TLS Private Key", + Optional: true, + }, + }, +} diff --git a/internal/schemas/sshconnection/schema.go b/internal/schemas/sshconnection/schema.go index 9d1b83c..ca246c0 100644 --- a/internal/schemas/sshconnection/schema.go +++ b/internal/schemas/sshconnection/schema.go @@ -6,37 +6,39 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) -var SchemaName = "ssh_connection" -var SchemaBlock = schema.SingleNestedBlock{ - MarkdownDescription: "Host connection details", - Attributes: map[string]schema.Attribute{ - "host": schema.StringAttribute{ - MarkdownDescription: "Host Machine address", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), +var ( + SchemaName = "ssh_connection" + SchemaBlockV0 = schema.SingleNestedBlock{ + MarkdownDescription: "Host connection details", + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + MarkdownDescription: "Host Machine address", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, }, - }, - "host_port": schema.StringAttribute{ - MarkdownDescription: "Host Machine port", - Optional: true, - }, - "user": schema.StringAttribute{ - MarkdownDescription: "Host Machine user", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), + "host_port": schema.StringAttribute{ + MarkdownDescription: "Host Machine port", + Optional: true, + }, + "user": schema.StringAttribute{ + MarkdownDescription: "Host Machine user", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "password": schema.StringAttribute{ + MarkdownDescription: "Host Machine password", + Optional: true, + Sensitive: true, + }, + "private_key": schema.StringAttribute{ + MarkdownDescription: "Host Machine RSA private key", + Optional: true, + Sensitive: true, }, }, - "password": schema.StringAttribute{ - MarkdownDescription: "Host Machine password", - Optional: true, - Sensitive: true, - }, - "private_key": schema.StringAttribute{ - MarkdownDescription: "Host Machine RSA private key", - Optional: true, - Sensitive: true, - }, - }, -} + } +) diff --git a/internal/vagrantbox/resource_models.go b/internal/vagrantbox/models/resource_models_v0.go similarity index 97% rename from internal/vagrantbox/resource_models.go rename to internal/vagrantbox/models/resource_models_v0.go index 81e1a19..0faef4b 100644 --- a/internal/vagrantbox/resource_models.go +++ b/internal/vagrantbox/models/resource_models_v0.go @@ -1,4 +1,4 @@ -package vagrantbox +package models import ( "terraform-provider-parallels-desktop/internal/schemas/authenticator" @@ -13,7 +13,7 @@ import ( ) // VirtualMachineStateResourceModel describes the resource data model. -type VagrantBoxResourceModel struct { +type VagrantBoxResourceModelV0 struct { Authenticator *authenticator.Authentication `tfsdk:"authenticator"` Host types.String `tfsdk:"host"` Orchestrator types.String `tfsdk:"orchestrator"` diff --git a/internal/vagrantbox/models/resource_models_v1.go b/internal/vagrantbox/models/resource_models_v1.go new file mode 100644 index 0000000..ca06bb1 --- /dev/null +++ b/internal/vagrantbox/models/resource_models_v1.go @@ -0,0 +1,43 @@ +package models + +import ( + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/prlctl" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" + "terraform-provider-parallels-desktop/internal/schemas/sharedfolder" + "terraform-provider-parallels-desktop/internal/schemas/vmconfig" + "terraform-provider-parallels-desktop/internal/schemas/vmspecs" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// VirtualMachineStateResourceModel describes the resource data model. +type VagrantBoxResourceModelV1 struct { + Authenticator *authenticator.Authentication `tfsdk:"authenticator"` + Host types.String `tfsdk:"host"` + Orchestrator types.String `tfsdk:"orchestrator"` + ID types.String `tfsdk:"id"` + ExternalIp types.String `tfsdk:"external_ip"` + InternalIp types.String `tfsdk:"internal_ip"` + OsType types.String `tfsdk:"os_type"` + BoxName types.String `tfsdk:"box_name"` + BoxVersion types.String `tfsdk:"box_version"` + VagrantFilePath types.String `tfsdk:"vagrant_file_path"` + CustomVagrantConfig types.String `tfsdk:"custom_vagrant_config"` + CustomParallelsConfig types.String `tfsdk:"custom_parallels_config"` + Name types.String `tfsdk:"name"` + Owner types.String `tfsdk:"owner"` + RunAfterCreate types.Bool `tfsdk:"run_after_create"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + Specs *vmspecs.VmSpecs `tfsdk:"specs"` + PostProcessorScripts []*postprocessorscript.PostProcessorScript `tfsdk:"post_processor_script"` + OnDestroyScript []*postprocessorscript.PostProcessorScript `tfsdk:"on_destroy_script"` + SharedFolder []*sharedfolder.SharedFolder `tfsdk:"shared_folder"` + ForceChanges types.Bool `tfsdk:"force_changes"` + Config *vmconfig.VmConfig `tfsdk:"config"` + PrlCtl []*prlctl.PrlCtlCmd `tfsdk:"prlctl"` + KeepRunning types.Bool `tfsdk:"keep_running"` + ReverseProxyHosts []*reverseproxy.ReverseProxyHost `tfsdk:"reverse_proxy_host"` +} diff --git a/internal/vagrantbox/resource.go b/internal/vagrantbox/resource.go index 66b6de5..9621e8a 100644 --- a/internal/vagrantbox/resource.go +++ b/internal/vagrantbox/resource.go @@ -10,9 +10,13 @@ import ( "terraform-provider-parallels-desktop/internal/common" "terraform-provider-parallels-desktop/internal/models" "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" "terraform-provider-parallels-desktop/internal/telemetry" + resource_models "terraform-provider-parallels-desktop/internal/vagrantbox/models" + "terraform-provider-parallels-desktop/internal/vagrantbox/schemas" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" @@ -39,7 +43,7 @@ func (r *VagrantBoxResource) Metadata(ctx context.Context, req resource.Metadata } func (r *VagrantBoxResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = getSchema(ctx) + resp.Schema = schemas.GetResourceSchemaV1(ctx) } func (r *VagrantBoxResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -60,7 +64,7 @@ func (r *VagrantBoxResource) Configure(ctx context.Context, req resource.Configu } func (r *VagrantBoxResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data VagrantBoxResourceModel + var data resource_models.VagrantBoxResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -234,13 +238,52 @@ func (r *VagrantBoxResource) Create(ctx context.Context, req resource.CreateRequ return } - // Starting the vm if requested - if data.RunAfterCreate.ValueBool() { + if len(data.ReverseProxyHosts) > 0 { + rpHostConfig := hostConfig + rpHostConfig.HostId = stoppedVm.HostId + rpHosts, updateDiag := updateReverseProxyHostsTarget(ctx, &data, rpHostConfig, stoppedVm) + if updateDiag.HasError() { + resp.Diagnostics.Append(updateDiag...) + return + } + + result, createDiag := reverseproxy.Create(ctx, rpHostConfig, rpHosts) + if createDiag.HasError() { + resp.Diagnostics.Append(createDiag...) + + if diag := reverseproxy.Delete(ctx, rpHostConfig, rpHosts); diag.HasError() { + tflog.Error(ctx, "Error deleting reverse proxy hosts") + } + + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, rpHostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, rpHostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + + apiclient.DeleteVm(ctx, rpHostConfig, data.ID.ValueString()) + } + return + } + + for i := range result { + data.ReverseProxyHosts[i].ID = result[i].ID + } + } + + // Starting the vm by default, otherwise we will stop the VM from being created + if data.RunAfterCreate.ValueBool() || data.KeepRunning.ValueBool() || (data.RunAfterCreate.IsUnknown() && data.KeepRunning.IsUnknown()) { if _, diag := common.EnsureMachineRunning(ctx, hostConfig, stoppedVm); diag.HasError() { resp.Diagnostics.Append(diag...) if data.ID.ValueString() != "" { // If we have an ID, we need to delete the machine apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) } return @@ -251,8 +294,37 @@ func (r *VagrantBoxResource) Create(ctx context.Context, req resource.CreateRequ resp.Diagnostics.Append(diag...) return } + } else { + // If we are not starting the machine, we will stop it + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, stoppedVm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } } + externalIp := "" + internalIp := "" + refreshVm, refreshDiag := apiclient.GetVm(ctx, hostConfig, response.ID) + if refreshDiag.HasError() { + resp.Diagnostics.Append(refreshDiag...) + return + } else { + externalIp = refreshVm.HostExternalIpAddress + internalIp = refreshVm.InternalIpAddress + } + + data.ExternalIp = types.StringValue(externalIp) + data.InternalIp = types.StringValue(internalIp) + data.OsType = types.StringValue(createdVM.OS) if data.OnDestroyScript != nil { for _, script := range data.OnDestroyScript { @@ -292,7 +364,7 @@ func (r *VagrantBoxResource) Create(ctx context.Context, req resource.CreateRequ } func (r *VagrantBoxResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data VagrantBoxResourceModel + var data resource_models.VagrantBoxResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -362,8 +434,8 @@ func (r *VagrantBoxResource) Read(ctx context.Context, req resource.ReadRequest, } func (r *VagrantBoxResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data VagrantBoxResourceModel - var currentData VagrantBoxResourceModel + var data resource_models.VagrantBoxResourceModelV1 + var currentData resource_models.VagrantBoxResourceModelV1 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -510,6 +582,81 @@ func (r *VagrantBoxResource) Update(ctx context.Context, req resource.UpdateRequ } } + if reverseproxy.ReverseProxyHostsDiff(data.ReverseProxyHosts, currentData.ReverseProxyHosts) { + copyCurrentRpHosts := reverseproxy.CopyReverseProxyHosts(currentData.ReverseProxyHosts) + copyRpHosts := reverseproxy.CopyReverseProxyHosts(data.ReverseProxyHosts) + + results, updateDiag := reverseproxy.Update(ctx, hostConfig, copyCurrentRpHosts, copyRpHosts) + if updateDiag.HasError() { + resp.Diagnostics.Append(updateDiag...) + revertResults, _ := reverseproxy.Revert(ctx, hostConfig, copyCurrentRpHosts, copyRpHosts) + for i := range revertResults { + data.ReverseProxyHosts[i].ID = revertResults[i].ID + } + return + } + + for i := range results { + data.ReverseProxyHosts[i].ID = results[i].ID + } + } else { + for i := range currentData.ReverseProxyHosts { + data.ReverseProxyHosts[i].ID = currentData.ReverseProxyHosts[i].ID + } + } + + // Starting the vm by default, otherwise we will stop the VM from being created + if data.RunAfterCreate.ValueBool() || data.KeepRunning.ValueBool() || (data.RunAfterCreate.IsUnknown() && data.KeepRunning.IsUnknown()) { + if _, diag := common.EnsureMachineRunning(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } + + _, diag := apiclient.GetVm(ctx, hostConfig, vm.ID) + if diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + } else { + // If we are not starting the machine, we will stop it + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, vm); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return + } + } + + externalIp := "" + internalIp := "" + refreshVm, refreshDiag := apiclient.GetVm(ctx, hostConfig, vm.ID) + if refreshDiag.HasError() { + resp.Diagnostics.Append(refreshDiag...) + return + } else { + externalIp = refreshVm.HostExternalIpAddress + internalIp = refreshVm.InternalIpAddress + } + + data.ExternalIp = types.StringValue(externalIp) + data.InternalIp = types.StringValue(internalIp) + data.ID = types.StringValue(vm.ID) if data.OnDestroyScript != nil { for _, script := range data.OnDestroyScript { @@ -546,7 +693,7 @@ func (r *VagrantBoxResource) Update(ctx context.Context, req resource.UpdateRequ } func (r *VagrantBoxResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data VagrantBoxResourceModel + var data resource_models.VagrantBoxResourceModelV1 // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -660,3 +807,71 @@ func (r *VagrantBoxResource) Delete(ctx context.Context, req resource.DeleteRequ func (r *VagrantBoxResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +func updateReverseProxyHostsTarget(ctx context.Context, data *resource_models.VagrantBoxResourceModelV1, hostConfig apiclient.HostConfig, targetVm *apimodels.VirtualMachine) ([]reverseproxy.ReverseProxyHost, diag.Diagnostics) { + resultDiagnostic := diag.Diagnostics{} + var refreshedVm *apimodels.VirtualMachine + var rpDiag diag.Diagnostics + refreshedVm, rpDiag = common.EnsureMachineHasInternalIp(ctx, hostConfig, targetVm) + if rpDiag.HasError() { + resultDiagnostic.Append(rpDiag...) + if data.ID.ValueString() != "" { + // If we have an ID, we need to delete the machine + apiclient.SetMachineState(ctx, hostConfig, data.ID.ValueString(), apiclient.MachineStateOpStop) + if _, diag := common.EnsureMachineStopped(ctx, hostConfig, refreshedVm); diag.HasError() { + return nil, diag + } + apiclient.DeleteVm(ctx, hostConfig, data.ID.ValueString()) + } + return nil, resultDiagnostic + } + + modifiedHosts := make([]reverseproxy.ReverseProxyHost, len(data.ReverseProxyHosts)) + for i := range data.ReverseProxyHosts { + host := reverseproxy.ReverseProxyHost{} + host.Host = data.ReverseProxyHosts[i].Host + host.Port = data.ReverseProxyHosts[i].Port + internalIp := refreshedVm.InternalIpAddress + emptyString := "" + + if data.ReverseProxyHosts[i].Cors != nil { + host.Cors = &reverseproxy.ReverseProxyCors{} + host.Cors.AllowedOrigins = data.ReverseProxyHosts[i].Cors.AllowedOrigins + host.Cors.AllowedMethods = data.ReverseProxyHosts[i].Cors.AllowedMethods + host.Cors.AllowedHeaders = data.ReverseProxyHosts[i].Cors.AllowedHeaders + host.Cors.Enabled = data.ReverseProxyHosts[i].Cors.Enabled + } + if data.ReverseProxyHosts[i].Tls != nil { + host.Tls = &reverseproxy.ReverseProxyTls{} + host.Tls.Certificate = data.ReverseProxyHosts[i].Tls.Certificate + host.Tls.PrivateKey = data.ReverseProxyHosts[i].Tls.PrivateKey + host.Tls.Enabled = data.ReverseProxyHosts[i].Tls.Enabled + } + if data.ReverseProxyHosts[i].TcpRoute != nil { + host.TcpRoute = &reverseproxy.ReverseProxyHostTcpRoute{} + host.TcpRoute.TargetPort = data.ReverseProxyHosts[i].TcpRoute.TargetPort + host.TcpRoute.TargetHost = types.StringValue(internalIp) + host.TcpRoute.TargetVmId = types.StringValue(emptyString) + } + + if len(data.ReverseProxyHosts[i].HttpRoute) > 0 { + host.HttpRoute = make([]*reverseproxy.ReverseProxyHttpRoute, len(data.ReverseProxyHosts[i].HttpRoute)) + for j := range modifiedHosts[i].HttpRoute { + httpRoute := reverseproxy.ReverseProxyHttpRoute{} + httpRoute.Path = data.ReverseProxyHosts[i].HttpRoute[j].Path + httpRoute.TargetHost = types.StringValue(internalIp) + httpRoute.TargetPort = data.ReverseProxyHosts[i].HttpRoute[j].TargetPort + httpRoute.TargetVmId = types.StringValue(emptyString) + httpRoute.Pattern = data.ReverseProxyHosts[i].HttpRoute[j].Pattern + httpRoute.Schema = data.ReverseProxyHosts[i].HttpRoute[j].Schema + httpRoute.RequestHeaders = data.ReverseProxyHosts[i].HttpRoute[j].RequestHeaders + httpRoute.ResponseHeaders = data.ReverseProxyHosts[i].HttpRoute[j].ResponseHeaders + host.HttpRoute[j] = &httpRoute + } + } + + modifiedHosts[i] = host + } + + return modifiedHosts, resultDiagnostic +} diff --git a/internal/vagrantbox/resource_schema.go b/internal/vagrantbox/schemas/resource_schema_v0.go similarity index 98% rename from internal/vagrantbox/resource_schema.go rename to internal/vagrantbox/schemas/resource_schema_v0.go index abcdccf..1d6211d 100644 --- a/internal/vagrantbox/resource_schema.go +++ b/internal/vagrantbox/schemas/resource_schema_v0.go @@ -1,4 +1,4 @@ -package vagrantbox +package schemas import ( "context" @@ -19,7 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) -func getSchema(ctx context.Context) schema.Schema { +func GetResourceSchemaV0(ctx context.Context) schema.Schema { return schema.Schema{ // This description is used by the documentation generator and the language server. MarkdownDescription: "Parallels Virtual Machine State Resource", diff --git a/internal/vagrantbox/schemas/resource_schema_v1.go b/internal/vagrantbox/schemas/resource_schema_v1.go new file mode 100644 index 0000000..c558392 --- /dev/null +++ b/internal/vagrantbox/schemas/resource_schema_v1.go @@ -0,0 +1,151 @@ +package schemas + +import ( + "context" + + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/postprocessorscript" + "terraform-provider-parallels-desktop/internal/schemas/prlctl" + "terraform-provider-parallels-desktop/internal/schemas/reverseproxy" + "terraform-provider-parallels-desktop/internal/schemas/sharedfolder" + "terraform-provider-parallels-desktop/internal/schemas/vmconfig" + "terraform-provider-parallels-desktop/internal/schemas/vmspecs" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func GetResourceSchemaV1(ctx context.Context) schema.Schema { + return schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Parallels Virtual Machine State Resource", + Blocks: map[string]schema.Block{ + authenticator.SchemaName: authenticator.SchemaBlock, + vmspecs.SchemaName: vmspecs.SchemaBlock, + postprocessorscript.SchemaName: postprocessorscript.SchemaBlock, + "on_destroy_script": postprocessorscript.SchemaBlock, + sharedfolder.SchemaName: sharedfolder.SchemaBlock, + vmconfig.SchemaName: vmconfig.SchemaBlock, + prlctl.SchemaName: prlctl.SchemaBlock, + reverseproxy.SchemaName: reverseproxy.HostBlockV0, + }, + Attributes: map[string]schema.Attribute{ + "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ + Create: true, + }), + "force_changes": schema.BoolAttribute{ + MarkdownDescription: "Force changes, this will force the VM to be stopped and started again", + Description: "Force changes, this will force the VM to be stopped and started again", + Optional: true, + }, + "host": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Host", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + }, + "orchestrator": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Orchestrator", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine Id", + Computed: true, + }, + "os_type": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine OS type", + Computed: true, + }, + "box_name": schema.StringAttribute{ + MarkdownDescription: "Vagrant box name", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("vagrant_file_path")), + }, + }, + "vagrant_file_path": schema.StringAttribute{ + MarkdownDescription: "Vagrant file path", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("vagrant_file_path")), + }, + }, + "box_version": schema.StringAttribute{ + MarkdownDescription: "Vagrant box version", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "custom_vagrant_config": schema.StringAttribute{ + MarkdownDescription: "Custom Vagrant config", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "custom_parallels_config": schema.StringAttribute{ + MarkdownDescription: "Custom Parallels config", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine name", + Required: true, + }, + "owner": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine owner", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "run_after_create": schema.BoolAttribute{ + MarkdownDescription: "Run after create", + Optional: true, + DeprecationMessage: "Use the `keep_running` attribute instead", + }, + "external_ip": schema.StringAttribute{ + MarkdownDescription: "VM external IP address", + Computed: true, + }, + "internal_ip": schema.StringAttribute{ + MarkdownDescription: "VM internal IP address", + Computed: true, + }, + "keep_running": schema.BoolAttribute{ + MarkdownDescription: "This will keep the VM running after the terraform apply", + Optional: true, + }, + }, + } +} diff --git a/internal/virtualmachine/datasource.go b/internal/virtualmachine/datasource.go index f5d51a7..a3ee11c 100644 --- a/internal/virtualmachine/datasource.go +++ b/internal/virtualmachine/datasource.go @@ -6,6 +6,8 @@ import ( "terraform-provider-parallels-desktop/internal/apiclient" "terraform-provider-parallels-desktop/internal/models" + data_models "terraform-provider-parallels-desktop/internal/virtualmachine/models" + "terraform-provider-parallels-desktop/internal/virtualmachine/schemas" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/types" @@ -46,24 +48,35 @@ func (d *VirtualMachinesDataSource) Metadata(_ context.Context, req datasource.M } func (d *VirtualMachinesDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = virtualMachineDataSourceSchema + resp.Schema = schemas.VirtualMachineDataSourceSchemaV1 } func (d *VirtualMachinesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data virtualMachinesDataSourceModel + var data data_models.VirtualMachinesDataSourceModelV1 resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } - if data.Host.ValueString() == "" { + // selecting if this is a standalone host or an orchestrator + isOrchestrator := false + var host string + if data.Orchestrator.ValueString() != "" { + isOrchestrator = true + host = data.Orchestrator.ValueString() + } else { + host = data.Host.ValueString() + } + + if host == "" { resp.Diagnostics.AddError("host cannot be empty", "Host cannot be null") return } hostConfig := apiclient.HostConfig{ - Host: data.Host.ValueString(), + Host: host, + IsOrchestrator: isOrchestrator, License: d.provider.License.ValueString(), Authorization: data.Authenticator, DisableTlsValidation: d.provider.DisableTlsValidation.ValueBool(), @@ -76,21 +89,24 @@ func (d *VirtualMachinesDataSource) Read(ctx context.Context, req datasource.Rea } for _, machine := range vms { - stateMachine := virtualMachineModel{ - HostIP: types.StringValue("-"), - ID: types.StringValue(machine.ID), - Name: types.StringValue(machine.Name), - Description: types.StringValue(machine.Description), - OSType: types.StringValue(machine.OS), - State: types.StringValue(machine.State), - Home: types.StringValue(machine.Home), + stateMachine := data_models.VirtualMachineModelV1{ + HostIP: types.StringValue("-"), + ID: types.StringValue(machine.ID), + Name: types.StringValue(machine.Name), + Description: types.StringValue(machine.Description), + OSType: types.StringValue(machine.OS), + State: types.StringValue(machine.State), + Home: types.StringValue(machine.Home), + ExternalIp: types.StringValue(machine.HostExternalIpAddress), + InternalIp: types.StringValue(machine.InternalIpAddress), + OrchestratorHostId: types.StringValue(machine.HostId), } data.Machines = append(data.Machines, stateMachine) } if data.Machines == nil { - data.Machines = make([]virtualMachineModel, 0) + data.Machines = make([]data_models.VirtualMachineModelV1, 0) } diags := resp.State.Set(ctx, &data) diff --git a/internal/virtualmachine/datasource_models.go b/internal/virtualmachine/models/datasource_models_v0.go similarity index 88% rename from internal/virtualmachine/datasource_models.go rename to internal/virtualmachine/models/datasource_models_v0.go index 64c2efd..6eff8ce 100644 --- a/internal/virtualmachine/datasource_models.go +++ b/internal/virtualmachine/models/datasource_models_v0.go @@ -1,4 +1,4 @@ -package virtualmachine +package models import ( "terraform-provider-parallels-desktop/internal/schemas/authenticator" @@ -8,15 +8,15 @@ import ( ) // virtualMachinesDataSourceModel represents the data source schema for the virtual_machines data source. -type virtualMachinesDataSourceModel struct { +type VirtualMachinesDataSourceModelV0 struct { Authenticator *authenticator.Authentication `tfsdk:"authenticator"` Host types.String `tfsdk:"host"` Filter *filter.Filter `tfsdk:"filter"` - Machines []virtualMachineModel `tfsdk:"machines"` + Machines []VirtualMachineModelV0 `tfsdk:"machines"` } // virtualMachineModel represents a virtual machine model with its properties. -type virtualMachineModel struct { +type VirtualMachineModelV0 struct { HostIP types.String `tfsdk:"host_ip"` // The IP address of the host machine. ID types.String `tfsdk:"id"` // The unique identifier of the virtual machine. Name types.String `tfsdk:"name"` // The name of the virtual machine. diff --git a/internal/virtualmachine/models/datasource_models_v1.go b/internal/virtualmachine/models/datasource_models_v1.go new file mode 100644 index 0000000..3a654e5 --- /dev/null +++ b/internal/virtualmachine/models/datasource_models_v1.go @@ -0,0 +1,31 @@ +package models + +import ( + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/filter" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// virtualMachinesDataSourceModel represents the data source schema for the virtual_machines data source. +type VirtualMachinesDataSourceModelV1 struct { + Authenticator *authenticator.Authentication `tfsdk:"authenticator"` + Host types.String `tfsdk:"host"` + Orchestrator types.String `tfsdk:"orchestrator"` + Filter *filter.Filter `tfsdk:"filter"` + Machines []VirtualMachineModelV1 `tfsdk:"machines"` +} + +// virtualMachineModel represents a virtual machine model with its properties. +type VirtualMachineModelV1 struct { + HostIP types.String `tfsdk:"host_ip"` // The IP address of the host machine. + ID types.String `tfsdk:"id"` // The unique identifier of the virtual machine. + ExternalIp types.String `tfsdk:"external_ip"` // The external IP address of the virtual machine. + InternalIp types.String `tfsdk:"internal_ip"` // The internal IP address of the virtual machine. + OrchestratorHostId types.String `tfsdk:"orchestrator_host_id"` // The unique identifier of the orchestrator host. + Name types.String `tfsdk:"name"` // The name of the virtual machine. + Description types.String `tfsdk:"description"` // The description of the virtual machine. + OSType types.String `tfsdk:"os_type"` // The type of the operating system installed on the virtual machine. + State types.String `tfsdk:"state"` // The state of the virtual machine. + Home types.String `tfsdk:"home"` // The path to the virtual machine home directory. +} diff --git a/internal/virtualmachine/data_source_schema.go b/internal/virtualmachine/schemas/datasource_schema_v0.go similarity index 55% rename from internal/virtualmachine/data_source_schema.go rename to internal/virtualmachine/schemas/datasource_schema_v0.go index 859c5a2..d9afc98 100644 --- a/internal/virtualmachine/data_source_schema.go +++ b/internal/virtualmachine/schemas/datasource_schema_v0.go @@ -1,4 +1,4 @@ -package virtualmachine +package schemas import ( "terraform-provider-parallels-desktop/internal/schemas/authenticator" @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" ) -var virtualMachineDataSourceSchema = schema.Schema{ +var VirtualMachineDataSourceSchemaV0 = schema.Schema{ MarkdownDescription: "Virtual Machine Data Source", Blocks: map[string]schema.Block{ authenticator.SchemaName: authenticator.SchemaBlock, @@ -22,25 +22,32 @@ var virtualMachineDataSourceSchema = schema.Schema{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "host_ip": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "The IP address of the host machine", + Computed: true, }, "id": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "The unique identifier of the virtual machine", + Computed: true, }, "name": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "The name of the virtual machine", + Computed: true, }, "description": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "The description of the virtual machine", + Computed: true, }, "os_type": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "The type of the operating system installed on the virtual machine", + Computed: true, }, "state": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "The state of the virtual machine", + Computed: true, }, "home": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "The path to the virtual machine home directory", + Computed: true, }, }, }, diff --git a/internal/virtualmachine/schemas/datasource_schema_v1.go b/internal/virtualmachine/schemas/datasource_schema_v1.go new file mode 100644 index 0000000..11872ae --- /dev/null +++ b/internal/virtualmachine/schemas/datasource_schema_v1.go @@ -0,0 +1,88 @@ +package schemas + +import ( + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + "terraform-provider-parallels-desktop/internal/schemas/filter" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var VirtualMachineDataSourceSchemaV1 = schema.Schema{ + MarkdownDescription: "Virtual Machine Data Source", + Blocks: map[string]schema.Block{ + authenticator.SchemaName: authenticator.SchemaBlock, + filter.SchemaName: filter.SchemaBlock, + }, + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Host", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + }, + "orchestrator": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Orchestrator", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + }, + "machines": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "host_ip": schema.StringAttribute{ + MarkdownDescription: "The IP address of the host machine", + Computed: true, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of the virtual machine", + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the virtual machine", + Computed: true, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "The description of the virtual machine", + Computed: true, + }, + "os_type": schema.StringAttribute{ + MarkdownDescription: "The type of the operating system installed on the virtual machine", + Computed: true, + }, + "state": schema.StringAttribute{ + MarkdownDescription: "The state of the virtual machine", + Computed: true, + }, + "home": schema.StringAttribute{ + MarkdownDescription: "The path to the virtual machine home directory", + Computed: true, + }, + "orchestrator_host_id": schema.StringAttribute{ + MarkdownDescription: "Orchestrator Host Id if the VM is running in an orchestrator", + Computed: true, + }, + "external_ip": schema.StringAttribute{ + MarkdownDescription: "VM external IP address", + Computed: true, + }, + "internal_ip": schema.StringAttribute{ + MarkdownDescription: "VM internal IP address", + Computed: true, + }, + }, + }, + }, + }, +} diff --git a/internal/virtualmachinestate/resource_models.go b/internal/virtualmachinestate/models/resource_model_v0.go similarity index 87% rename from internal/virtualmachinestate/resource_models.go rename to internal/virtualmachinestate/models/resource_model_v0.go index 70129e5..a3d3ae8 100644 --- a/internal/virtualmachinestate/resource_models.go +++ b/internal/virtualmachinestate/models/resource_model_v0.go @@ -1,4 +1,4 @@ -package virtualmachinestate +package models import ( "terraform-provider-parallels-desktop/internal/schemas/authenticator" @@ -7,7 +7,7 @@ import ( ) // VirtualMachineStateResourceModel describes the resource data model. -type VirtualMachineStateResourceModel struct { +type VirtualMachineStateResourceModelV0 struct { Authenticator *authenticator.Authentication `tfsdk:"authenticator"` Host types.String `tfsdk:"host"` ID types.String `tfsdk:"id"` diff --git a/internal/virtualmachinestate/models/resource_model_v1.go b/internal/virtualmachinestate/models/resource_model_v1.go new file mode 100644 index 0000000..c9ea0ed --- /dev/null +++ b/internal/virtualmachinestate/models/resource_model_v1.go @@ -0,0 +1,17 @@ +package models + +import ( + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// VirtualMachineStateResourceModel describes the resource data model. +type VirtualMachineStateResourceModelV1 struct { + Authenticator *authenticator.Authentication `tfsdk:"authenticator"` + Orchestrator types.String `tfsdk:"orchestrator"` + Host types.String `tfsdk:"host"` + ID types.String `tfsdk:"id"` + Operation types.String `tfsdk:"operation"` + CurrentState types.String `tfsdk:"current_state"` +} diff --git a/internal/virtualmachinestate/resource.go b/internal/virtualmachinestate/resource.go index ab09fa7..cbc3c24 100644 --- a/internal/virtualmachinestate/resource.go +++ b/internal/virtualmachinestate/resource.go @@ -7,6 +7,8 @@ import ( "terraform-provider-parallels-desktop/internal/apiclient" "terraform-provider-parallels-desktop/internal/models" + resource_models "terraform-provider-parallels-desktop/internal/virtualmachinestate/models" + "terraform-provider-parallels-desktop/internal/virtualmachinestate/schemas" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -34,7 +36,7 @@ func (r *VirtualMachineStateResource) Metadata(ctx context.Context, req resource } func (r *VirtualMachineStateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = virtualMachineStateResourceSchema + resp.Schema = schemas.VirtualMachineStateResourceSchemaV1 } func (r *VirtualMachineStateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -55,19 +57,32 @@ func (r *VirtualMachineStateResource) Configure(ctx context.Context, req resourc } func (r *VirtualMachineStateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data VirtualMachineStateResourceModel + var data resource_models.VirtualMachineStateResourceModelV1 resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } - if data.Host.ValueString() == "" { + + // selecting if this is a standalone host or an orchestrator + isOrchestrator := false + var host string + if data.Orchestrator.ValueString() != "" { + isOrchestrator = true + host = data.Orchestrator.ValueString() + } else { + host = data.Host.ValueString() + } + + if host == "" { resp.Diagnostics.AddError("host cannot be empty", "Host cannot be null") return } + hostConfig := apiclient.HostConfig{ - Host: data.Host.ValueString(), + Host: host, + IsOrchestrator: isOrchestrator, License: r.provider.License.ValueString(), Authorization: data.Authenticator, DisableTlsValidation: r.provider.DisableTlsValidation.ValueBool(), @@ -103,7 +118,7 @@ func (r *VirtualMachineStateResource) Create(ctx context.Context, req resource.C } func (r *VirtualMachineStateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data VirtualMachineStateResourceModel + var data resource_models.VirtualMachineStateResourceModelV1 resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -111,13 +126,24 @@ func (r *VirtualMachineStateResource) Read(ctx context.Context, req resource.Rea return } - if data.Host.ValueString() == "" { + // selecting if this is a standalone host or an orchestrator + isOrchestrator := false + var host string + if data.Orchestrator.ValueString() != "" { + isOrchestrator = true + host = data.Orchestrator.ValueString() + } else { + host = data.Host.ValueString() + } + + if host == "" { resp.Diagnostics.AddError("host cannot be empty", "Host cannot be null") return } hostConfig := apiclient.HostConfig{ - Host: data.Host.ValueString(), + Host: host, + IsOrchestrator: isOrchestrator, License: r.provider.License.ValueString(), Authorization: data.Authenticator, DisableTlsValidation: r.provider.DisableTlsValidation.ValueBool(), @@ -143,7 +169,7 @@ func (r *VirtualMachineStateResource) Read(ctx context.Context, req resource.Rea } func (r *VirtualMachineStateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data VirtualMachineStateResourceModel + var data resource_models.VirtualMachineStateResourceModelV1 resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) @@ -151,18 +177,29 @@ func (r *VirtualMachineStateResource) Update(ctx context.Context, req resource.U return } - if data.Host.ValueString() == "" { - resp.Diagnostics.AddError("host cannot be empty", "Host cannot be null") + if data.ID.IsNull() { + resp.Diagnostics.AddError("Id is required", "Id is required") return } - if data.ID.IsNull() { - resp.Diagnostics.AddError("Id is required", "Id is required") + // selecting if this is a standalone host or an orchestrator + isOrchestrator := false + var host string + if data.Orchestrator.ValueString() != "" { + isOrchestrator = true + host = data.Orchestrator.ValueString() + } else { + host = data.Host.ValueString() + } + + if host == "" { + resp.Diagnostics.AddError("host cannot be empty", "Host cannot be null") return } hostConfig := apiclient.HostConfig{ - Host: data.Host.ValueString(), + Host: host, + IsOrchestrator: isOrchestrator, License: r.provider.License.ValueString(), Authorization: data.Authenticator, DisableTlsValidation: r.provider.DisableTlsValidation.ValueBool(), @@ -217,7 +254,7 @@ func (r *VirtualMachineStateResource) Update(ctx context.Context, req resource.U } func (r *VirtualMachineStateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data VirtualMachineStateResourceModel + var data resource_models.VirtualMachineStateResourceModelV1 // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) diff --git a/internal/virtualmachinestate/resource_schema.go b/internal/virtualmachinestate/schemas/resource_schema_v0.go similarity index 94% rename from internal/virtualmachinestate/resource_schema.go rename to internal/virtualmachinestate/schemas/resource_schema_v0.go index 89fc0b8..f430af3 100644 --- a/internal/virtualmachinestate/resource_schema.go +++ b/internal/virtualmachinestate/schemas/resource_schema_v0.go @@ -1,4 +1,4 @@ -package virtualmachinestate +package schemas import ( "terraform-provider-parallels-desktop/internal/schemas/authenticator" @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) -var virtualMachineStateResourceSchema = schema.Schema{ +var VirtualMachineStateResourceSchemaV0 = schema.Schema{ MarkdownDescription: "Parallels Virtual Machine State Resource\n Use this to set a virtual machine to a desired state.", Blocks: map[string]schema.Block{ authenticator.SchemaName: authenticator.SchemaBlock, diff --git a/internal/virtualmachinestate/schemas/resource_schema_v1.go b/internal/virtualmachinestate/schemas/resource_schema_v1.go new file mode 100644 index 0000000..75d51a0 --- /dev/null +++ b/internal/virtualmachinestate/schemas/resource_schema_v1.go @@ -0,0 +1,65 @@ +package schemas + +import ( + "terraform-provider-parallels-desktop/internal/schemas/authenticator" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var VirtualMachineStateResourceSchemaV1 = schema.Schema{ + MarkdownDescription: "Parallels Virtual Machine State Resource\n Use this to set a virtual machine to a desired state.", + Blocks: map[string]schema.Block{ + authenticator.SchemaName: authenticator.SchemaBlock, + }, + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Host", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + }, + "orchestrator": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Orchestrator", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("orchestrator"), + path.MatchRoot("host"), + }...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "Virtual Machine Id", + Required: true, + }, + "operation": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Virtual Machine desired state", + Validators: []validator.String{ + stringvalidator.OneOf("start", "stop", "suspend", "pause", "resume", "restart"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "current_state": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Virtual Machine current state", + }, + }, +}