diff --git a/internal/apiclient/apimodels/virtual_machine.go b/internal/apiclient/apimodels/virtual_machine.go index 59566d7..d6e0a9f 100644 --- a/internal/apiclient/apimodels/virtual_machine.go +++ b/internal/apiclient/apimodels/virtual_machine.go @@ -3,6 +3,8 @@ package apimodels type VirtualMachine struct { User string `json:"user"` ID string `json:"ID"` + Host string `json:"Host"` + HostUrl string `json:"host_url"` HostId string `json:"host_id"` HostExternalIpAddress string `json:"host_external_ip_address"` InternalIpAddress string `json:"internal_ip_address"` diff --git a/internal/common/ensure_machine_stopped.go b/internal/common/ensure_machine_stopped.go index 3e04c59..7d1d693 100644 --- a/internal/common/ensure_machine_stopped.go +++ b/internal/common/ensure_machine_stopped.go @@ -43,6 +43,10 @@ func EnsureMachineStopped(ctx context.Context, hostConfig apiclient.HostConfig, if checkVmDiag.HasError() { diagnostics.Append(checkVmDiag...) } + if updatedVm == nil { + diagnostics.AddError("error stopping vm", "VM not found") + return returnVm, diagnostics + } // All if good, break out of the loop if updatedVm.State == "stopped" { diff --git a/internal/remoteimage/models/resource_models_v2.go b/internal/remoteimage/models/resource_models_v2.go new file mode 100644 index 0000000..c33f5df --- /dev/null +++ b/internal/remoteimage/models/resource_models_v2.go @@ -0,0 +1,45 @@ +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 RemoteVmResourceModelV2 struct { + Authenticator *authenticator.Authentication `tfsdk:"authenticator"` + Host types.String `tfsdk:"host"` + HostUrl types.String `tfsdk:"host_url"` + 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 5629ab0..f5fe38a 100644 --- a/internal/remoteimage/resource.go +++ b/internal/remoteimage/resource.go @@ -43,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 = schemas.GetRemoteImageSchemaV1(ctx) + resp.Schema = schemas.GetRemoteImageSchemaV2(ctx) } func (r *RemoteVmResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -64,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 models.RemoteVmResourceModelV1 + var data models.RemoteVmResourceModelV2 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -369,11 +369,12 @@ func (r *RemoteVmResource) Create(ctx context.Context, req resource.CreateReques externalIp := "" internalIp := "" + data.HostUrl = types.StringValue(stoppedVm.HostUrl) retryAttempts := 10 var refreshVm *apimodels.VirtualMachine var refreshDiag diag.Diagnostics for { - refreshVm, refreshDiag = apiclient.GetVm(ctx, hostConfig, refreshVm.ID) + refreshVm, refreshDiag = apiclient.GetVm(ctx, hostConfig, stoppedVm.ID) if !refreshDiag.HasError() { externalIp = refreshVm.HostExternalIpAddress internalIp = refreshVm.InternalIpAddress @@ -450,7 +451,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 models.RemoteVmResourceModelV1 + var data models.RemoteVmResourceModelV2 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -522,8 +523,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 models.RemoteVmResourceModelV1 - var currentData models.RemoteVmResourceModelV1 + var data models.RemoteVmResourceModelV2 + var currentData models.RemoteVmResourceModelV2 telemetrySvc := telemetry.Get(ctx) telemetryEvent := telemetry.NewTelemetryItem( @@ -753,6 +754,7 @@ func (r *RemoteVmResource) Update(ctx context.Context, req resource.UpdateReques externalIp := "" internalIp := "" + data.HostUrl = types.StringValue(vm.HostUrl) retryAttempts := 10 var refreshVm *apimodels.VirtualMachine var refreshDiag diag.Diagnostics @@ -860,7 +862,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 models.RemoteVmResourceModelV1 + var data models.RemoteVmResourceModelV2 // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -988,11 +990,16 @@ func (r *RemoteVmResource) ImportState(ctx context.Context, req resource.ImportS func (r *RemoteVmResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { v0Schema := schemas.GetRemoteImageSchemaV0(ctx) + V1Schema := schemas.GetRemoteImageSchemaV1(ctx) return map[int64]resource.StateUpgrader{ 0: { PriorSchema: &v0Schema, StateUpgrader: UpgradeStateToV1, }, + 1: { + PriorSchema: &V1Schema, + StateUpgrader: UpgradeStateToV2, + }, } } @@ -1038,7 +1045,50 @@ func UpgradeStateToV1(ctx context.Context, req resource.UpgradeStateRequest, res 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) { +func UpgradeStateToV2(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData models.RemoteVmResourceModelV1 + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := models.RemoteVmResourceModelV2{ + Authenticator: priorStateData.Authenticator, + Host: priorStateData.Host, + HostUrl: types.StringUnknown(), + Orchestrator: priorStateData.Orchestrator, + ID: priorStateData.ID, + OsType: priorStateData.OsType, + ExternalIp: priorStateData.ExternalIp, + InternalIp: priorStateData.InternalIp, + OrchestratorHostId: priorStateData.OrchestratorHostId, + 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: priorStateData.KeepRunning, + ReverseProxyHosts: priorStateData.ReverseProxyHosts, + } + + println(fmt.Sprintf("Upgrading state from version %v", upgradedStateData)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &upgradedStateData)...) +} + +func updateReverseProxyHostsTarget(ctx context.Context, data *models.RemoteVmResourceModelV2, hostConfig apiclient.HostConfig, targetVm *apimodels.VirtualMachine) ([]reverseproxy.ReverseProxyHost, diag.Diagnostics) { resultDiagnostic := diag.Diagnostics{} var refreshedVm *apimodels.VirtualMachine var rpDiag diag.Diagnostics diff --git a/internal/remoteimage/schemas/resource_schema_v2.go b/internal/remoteimage/schemas/resource_schema_v2.go new file mode 100644 index 0000000..a63707e --- /dev/null +++ b/internal/remoteimage/schemas/resource_schema_v2.go @@ -0,0 +1,150 @@ +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 GetRemoteImageSchemaV2(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, + }, + "host_url": schema.StringAttribute{ + MarkdownDescription: "Parallels Desktop DevOps Host URL", + Computed: true, + }, + }, + } +}