diff --git a/docs/guides/dedicated_server_migration.md b/docs/guides/dedicated_server_migration.md new file mode 100644 index 000000000..9553834a3 --- /dev/null +++ b/docs/guides/dedicated_server_migration.md @@ -0,0 +1,105 @@ +--- +page_title: "Migrating dedicated servers from previous versions to v2.2.0" +--- + +Version v2.0.0 of OVHcloud Terraform provider introduced a breaking change on the resources related to dedicated servers, mainly: +- Deletion of resource `ovh_dedicated_server_install_task` that has been replaced by a new resource `ovh_dedicated_server_reinstall_task` +- The parameters of resource `ovh_dedicated_server` have been updated to reflect the changes on parameters needed to reinstall a dedicated server + +The complete changelog can be found [here](https://github.com/ovh/terraform-provider-ovh/releases/tag/v2.0.0). + +This guide explains how to migrate your existing configuration relying on resource `ovh_dedicated_server_install_task` to a new configuration compatible with version v2.2.0 of the provider without triggering a reinstallation of your dedicated servers. + +!> This documentation presents a direct migration path between v1.6.0 and v2.2.0. To make this transition easier, we released a version v1.8.0 of the provider that includes both deprecated resources and the new ones. The migration steps are the same, but this version allows a more gradual shift towards v2.2.0. + +## First step: import your dedicated servers in the state + +From version v2.0.0 and later, the preferred way to manage dedicated servers installation details is through usage of resource `ovh_dedicated_server`. As a result, if you don't already have your dedicated servers declared in your Terraform configuration, you must import them. + +You can use an `import` block like the following: + +```terraform +import { + id = "nsxxxxxxx.ip-xx-xx-xx.eu" + to = ovh_dedicated_server.srv +} + +resource "ovh_dedicated_server" "srv" {} +``` + +-> If you are doing a first migration to v1.8.0, you should add the following parameter `prevent_install_on_import = true` to the dedicated server resource. This guarantees you that the server won't be reinstalled after import, even if you have a diff on the reinstall-related parameters. + +To finish importing the resource into your Terraform state, you should run: + +```sh +terraform apply +``` + +## Second step: backport your previous task details into the imported resource + +This step is manual and requires you to convert the previous installation details from resource `ovh_dedicated_server_install_task` to the new fields of resource `ovh_dedicated_server`: `os`, `customizations`, `properties` and `storage`. + +Let's take an example: if you previously used the following configuration: + +```terraform +resource "ovh_dedicated_server_install_task" "server_install" { + service_name = "nsxxxxxxx.ip-xx-xx-xx.eu" + template_name = "debian12_64" + details { + custom_hostname = "mytest" + } + user_metadata { + key = "sshKey" + value = "ssh-ed25519 AAAAC3..." + } + user_metadata { + key = "postInstallationScript" + value = <<-EOF + #!/bin/bash + echo "coucou postInstallationScript" > /opt/coucou + cat /etc/machine-id >> /opt/coucou + date "+%Y-%m-%d %H:%M:%S" --utc >> /opt/coucou + EOF + } +} +``` + +You can replace it by the following one: + +```terraform +resource "ovh_dedicated_server" "srv" { + customizations = { + hostname = "mytest" + post_installation_script = "IyEvYmluL2Jhc2gKZWNobyAiY291Y291IHBvc3RJbnN0YWxsYXRpb25TY3JpcHQiID4gL29wdC9jb3Vjb3UKY2F0IC9ldGMvbWFjaGluZS1pZCAgPj4gL29wdC9jb3Vjb3UKZGF0ZSAiKyVZLSVtLSVkICVIOiVNOiVTIiAtLXV0YyA+PiAvb3B0L2NvdWNvdQo=" + ssh_key = "ssh-ed25519 AAAAC3..." + } + os = "debian12_64" + properties = null + storage = null +} +``` + +You can check the documentation of resource `ovh_dedicated_server` to see what inputs are available for the reinstallation-related fields. +The documentation of resource `ovh_dedicated_server_reinstall_task` also includes several examples of configuration. + +## Third step: make sure your server is not reinstalled unintentionally + +You should add the following piece of configuration in the declaration of your dedicated server resource in order to avoid a reinstallation on the next `terraform apply`: + +```terraform +resource "ovh_dedicated_server" "srv" { + # + # ... resource fields + # + + lifecycle { + ignore_changes = [os, customizations, properties, storage] + } +} +``` + +This is needed because there is no API endpoint that returns the previous installation parameters of a dedicated server. The goal here is to migrate your old configuration to the new format without triggering a reinstallation. + +## Fourth step: remove the lifecycle block + +After a while, whenever you need to trigger a reinstallation of your dedicated server, you can just remove the `lifecycle` field from your configuration and run `terraform apply`. \ No newline at end of file diff --git a/docs/resources/dedicated_server.md b/docs/resources/dedicated_server.md index b0279d941..cae7b0e60 100644 --- a/docs/resources/dedicated_server.md +++ b/docs/resources/dedicated_server.md @@ -90,6 +90,7 @@ resource "ovh_dedicated_server" "server" { * `configuration` - Representation of a configuration item to personalize product * `label` - (Required) Identifier of the resource * `value` - (Required) Path to the resource in API.OVH.COM +* `service_name` - (Optional) The service_name of your dedicated server. This field can be used to avoid ordering a dedicated server at creation and just create the resource using an already existing service ### Editable fields of a dedicated server @@ -103,26 +104,27 @@ resource "ovh_dedicated_server" "server" { * `state` - All states a Dedicated can in (error, hacked, hackedBlocked, ok) ### Arguments used to reinstall a dedicated server + * `os` - Operating System to install * `customizations` - Customization of the OS configuration - * `configDriveUserData` -Config Drive UserData - * `efiBootloaderPath` - Path of the EFI bootloader from the OS installed on the server + * `config_drive_user_data` - Config Drive UserData + * `efi_bootloader_path` - Path of the EFI bootloader from the OS installed on the server * `hostname` - Custom hostname - * `httpHeaders` - Image HTTP Headers - * `imageCheckSum` - Image checksum - * `imageCheckSumType` - Checksum type - * `imageType` - Image Type - * `imageURL` - Image URL + * `http_headers` - Image HTTP Headers + * `image_check_sum` - Image checksum + * `image_check_sum_type` - Checksum type + * `image_type` - Image Type + * `image_url` - Image URL * `language` - Display Language - * `postInstallationScript` - Post-Installation Script - * `postInstallationScriptExtension` - Post-Installation Script File Extension - * `sshKey` - SSH Public Key + * `post_installation_script` - Post-Installation Script + * `post_installation_script_extension` - Post-Installation Script File Extension + * `ssh_key` - SSH Public Key * `storage` - Storage customization - * `diskGroupId` - Disk group id - * `hardwareRaid` - Hardware Raid configurations + * `disk_group_id` - Disk group id + * `hardware_raid` - Hardware Raid configurations * `arrays` - Number of arrays * `disks` - Total number of disks in the disk group involved in the hardware raid configuration - * `raidLevel` - Hardware raid type + * `raid_level` - Hardware raid type * `spares` - Number of disks in the disk group involved in the spare * `partitioning` - Partitioning configuration * `disks` - Total number of disks in the disk group involved in the partitioning configuration @@ -130,13 +132,19 @@ resource "ovh_dedicated_server" "server" { * `extras` - Partition extras parameters * `lv` - LVM-specific parameters * `zp` - ZFS-specific parameters - * `fileSystem` - File system type - * `mountPoint` - Mount point - * `raidLevel` - Software raid type + * `file_system` - File system type + * `mount_point` - Mount point + * `raid_level` - Software raid type * `size` - Partition size in MiB - * `schemeName` - Partitioning scheme (if applicable with selected operating system) + * `scheme_name` - Partitioning scheme (if applicable with selected operating system) * `properties` - Arbitrary properties to pass to cloud-init's config drive datasource +### Arguments used to control the lifecycle of a dedicated server + +* `keep_service_after_destroy` - Avoid termination of the service when deleting the resource (when using this parameter, make sure to apply your configuration before running the destroy so that the value is set in the state) +* `prevent_install_on_create` - Prevent server installation after it has been delivered +* `prevent_install_on_import` - Defines whether a reinstallation of the server is allowed after importing it if there is a modification on the installation parameters + ## Attributes Reference * `service_name` - The service_name of your dedicated server diff --git a/docs/resources/dedicated_server_reinstall_task.md b/docs/resources/dedicated_server_reinstall_task.md index 98d880563..4ac9cf8bc 100644 --- a/docs/resources/dedicated_server_reinstall_task.md +++ b/docs/resources/dedicated_server_reinstall_task.md @@ -282,11 +282,3 @@ The following attributes are exported: * `function` - Function name (should be `hardInstall`). * `start_date` - Task creation date in RFC3339 format. * `status` - Task status (should be `done`) - -## Import - -Installation task can be imported using the `service_name` (`nsXXXX.ip...`) of the baremetal server, the `operating_system` used and ths `task_id`, separated by "/" E.g., - -```bash -terraform import ovh_dedicated_server_reinstall_task nsXXXX.ipXXXX/operating_system/12345 -``` diff --git a/ovh/order_resource_gen.go b/ovh/order_resource_gen.go index ad58bd6d3..88eb442b3 100644 --- a/ovh/order_resource_gen.go +++ b/ovh/order_resource_gen.go @@ -169,7 +169,6 @@ func OrderResourceSchema(ctx context.Context) schema.Schema { }, CustomType: ovhtypes.NewTfListNestedType[PlanValue](ctx), Optional: true, - Computed: true, PlanModifiers: []planmodifier.List{ listplanmodifier.RequiresReplaceIfConfigured(), }, @@ -235,7 +234,6 @@ func OrderResourceSchema(ctx context.Context) schema.Schema { }, CustomType: ovhtypes.NewTfListNestedType[PlanOptionValue](ctx), Optional: true, - Computed: true, }, }, } diff --git a/ovh/resource_dedicated_server.go b/ovh/resource_dedicated_server.go index 57f49a209..e16591895 100644 --- a/ovh/resource_dedicated_server.go +++ b/ovh/resource_dedicated_server.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net/url" + "strconv" "github.com/ovh/go-ovh/ovh" @@ -14,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ovhtypes "github.com/ovh/terraform-provider-ovh/v2/ovh/types" ) @@ -54,12 +56,8 @@ func (d *dedicatedServerResource) Schema(ctx context.Context, req resource.Schem } func (d *dedicatedServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - // Here we force the attribute "plan" to an empty array because it won't be fetched by the Read function. - // If we don't do this, Terraform always shows a diff on the following plans (null => []), due to the - // plan modifier RequiresReplace that initializes the attribute to its zero-value (an empty array). - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("plan"), ovhtypes.TfListNestedValue[PlanValue]{ - ListValue: basetypes.NewListValueMust(PlanValue{}.Type(ctx), make([]attr.Value, 0)), - })...) + resp.Diagnostics.Append(resp.Private.SetKey(ctx, "is_imported", []byte("true"))...) + resp.Diagnostics.Append(resp.Private.SetKey(ctx, "run_count", []byte("0"))...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) } @@ -72,41 +70,50 @@ func (r *dedicatedServerResource) Create(ctx context.Context, req resource.Creat return } - order := data.ToOrder() - if err := orderCreate(order, r.config, "baremetalServers", false); err != nil { - resp.Diagnostics.AddError("failed to create order", err.Error()) - return - } - orderID := order.Order.OrderId.ValueInt64() - data.Order = OrderValue{ - state: attr.ValueStateKnown, - OrderId: ovhtypes.NewTfInt64Value(orderID), - } + // If service_name is not provided, it means dedicated server has to be ordered + if data.ServiceName.IsNull() || data.ServiceName.IsUnknown() { + order := data.ToOrder() + if err := orderCreate(order, r.config, "baremetalServers", false); err != nil { + resp.Diagnostics.AddError("failed to create order", err.Error()) + return + } + orderID := order.Order.OrderId.ValueInt64() + data.Order = OrderValue{ + state: attr.ValueStateKnown, + OrderId: ovhtypes.NewTfInt64Value(orderID), + } - // Wait for order to be completed - _, err := waitOrderCompletionV2(ctx, r.config, orderID) - if err != nil { - timeout := &retry.TimeoutError{} - if errors.As(err, &timeout) { - // Delivery took too long, just store the order ID and leave. - // Resource will have to be untainted before next apply (cf: https://discuss.hashicorp.com/t/partial-resource-create-tainted-state/48905). - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) - resp.Diagnostics.AddError("still waiting for order to be completed", fmt.Sprintf("Order %d not delivered yet, saving the ID for later", orderID)) - } else { - // Got a real error, return it and don't save anything in the state - resp.Diagnostics.AddError("error waiting for order", err.Error()) + // Wait for order to be completed + _, err := waitOrderCompletionV2(ctx, r.config, orderID) + if err != nil { + timeout := &retry.TimeoutError{} + if errors.As(err, &timeout) { + // Delivery took too long, just store the order ID and leave. + // Resource will have to be untainted before next apply (cf: https://discuss.hashicorp.com/t/partial-resource-create-tainted-state/48905). + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.AddError("still waiting for order to be completed", fmt.Sprintf("Order %d not delivered yet, saving the ID for later", orderID)) + } else { + // Got a real error, return it and don't save anything in the state + resp.Diagnostics.AddError("error waiting for order", err.Error()) + } + return + } + + // Find service name from order + r.getServiceName(ctx, &data, orderID, resp.Diagnostics) + if resp.Diagnostics.HasError() { + return } - return } - // Find service name from order - r.getServiceName(ctx, &data, orderID, resp.Diagnostics) - if resp.Diagnostics.HasError() { + // Reinstall server if not blocked by configuration + if err := r.reinstallDedicatedServer(ctx, data.PreventInstallOnCreate.ValueBool(), nil, &data); err != nil { + resp.Diagnostics.AddError("failed to reinstall server", err.Error()) return } // Update resource - r.updateDedicatedServerResource(ctx, nil, &data, &responseData, resp.Diagnostics) + r.updateDedicatedServerResource(ctx, nil, &data, &responseData, &resp.Diagnostics) if resp.Diagnostics.HasError() { // TODO: not sure we should return here, maybe save the state instead return @@ -166,10 +173,35 @@ func (r *dedicatedServerResource) Read(ctx context.Context, req resource.ReadReq return } - data.MergeWith(&responseData) + responseData.MergeWith(&data) + + // Check if resource has just been imported. If it is the case, increase + // the run counter each time we go through this function. The run counter + // value is used in the Update function to decide if the server should be + // reinstalled or not. + isImported, privDiags := req.Private.GetKey(ctx, "is_imported") + if privDiags.HasError() { + resp.Diagnostics.Append(privDiags...) + return + } + + if isImported != nil && string(isImported) == "true" { + runCounter, privDiags := req.Private.GetKey(ctx, "run_count") + if privDiags.HasError() { + resp.Diagnostics.Append(privDiags...) + return + } + count, err := strconv.Atoi(string(runCounter)) + if err != nil { + resp.Diagnostics.AddError("failed to read run_count from private state", err.Error()) + return + } + + resp.Private.SetKey(ctx, "run_count", []byte(strconv.Itoa(count+1))) + } // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &responseData)...) } func (r *dedicatedServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -216,8 +248,48 @@ func (r *dedicatedServerResource) Update(ctx context.Context, req resource.Updat } } + // Check if resource has just been imported, and remove key from private state. + // We need this information to know if a reinstall should be forced at this point. + isImported, privDiags := req.Private.GetKey(ctx, "is_imported") + if privDiags.HasError() { + resp.Diagnostics.Append(privDiags...) + return + } + resp.Private.SetKey(ctx, "is_imported", nil) + + // Decide if server installation should be blocked. This happens when resource has + // just been imported and the parameter "prevent_install_on_import" is true. + var preventReinstall bool + if isImported != nil && string(isImported) == "true" { + runCounter, privDiags := req.Private.GetKey(ctx, "run_count") + if privDiags.HasError() { + resp.Diagnostics.Append(privDiags...) + return + } + resp.Private.SetKey(ctx, "run_count", nil) + + count, err := strconv.Atoi(string(runCounter)) + if err != nil { + resp.Diagnostics.AddError("failed to read run_count from private state", err.Error()) + return + } + + // When "is_imported" is true, check parameter "prevent_install_on_import" to decide + // if we should allow a server reinstallation. + // If "run_count" is > 1, it means that Update was not called at import time, so we just + // ignore this parameter: changing the value of "prevent_install_on_import" after import + // should not have any effect. + preventReinstall = count == 1 && planData.PreventInstallOnImport.ValueBool() + } + + // Reinstall server (if needed and not blocked) + if err := r.reinstallDedicatedServer(ctx, preventReinstall, &stateData, &planData); err != nil { + resp.Diagnostics.AddError("failed to reinstall server", err.Error()) + return + } + // Update resource - r.updateDedicatedServerResource(ctx, &stateData, &planData, &responseData, resp.Diagnostics) + r.updateDedicatedServerResource(ctx, &stateData, &planData, &responseData, &resp.Diagnostics) if resp.Diagnostics.HasError() { // TODO: not sure we should return here, maybe save the state instead return @@ -232,6 +304,11 @@ func (r *dedicatedServerResource) Update(ctx context.Context, req resource.Updat responseData.Properties = planData.Properties responseData.Storage = planData.Storage + // Same thing for the flags to control reinstallation, set the plan value explicitly + responseData.PreventInstallOnCreate = planData.PreventInstallOnCreate + responseData.PreventInstallOnImport = planData.PreventInstallOnImport + responseData.KeepServiceAfterDestroy = planData.KeepServiceAfterDestroy + // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &responseData)...) } @@ -245,6 +322,11 @@ func (r *dedicatedServerResource) Delete(ctx context.Context, req resource.Delet return } + if data.KeepServiceAfterDestroy.ValueBool() { + log.Print("Will remove the resource without terminating service") + return + } + serviceName := data.ServiceName.ValueString() if serviceName == "" { resp.Diagnostics.AddError("cannot terminate resource, missing service_name", @@ -279,20 +361,13 @@ func (r *dedicatedServerResource) Delete(ctx context.Context, req resource.Delet } } -func (r *dedicatedServerResource) updateDedicatedServerResource(ctx context.Context, stateData, planData, responseData *DedicatedServerModel, diags diag.Diagnostics) { - // Check if server needs to be reinstalled - var shouldReinstall bool - if stateData != nil { - if stateData.Os.ValueString() != planData.Os.ValueString() || - !stateData.Customizations.Equal(planData.Customizations) || - !stateData.Storage.Equal(planData.Storage) || - !stateData.Properties.Equal(planData.Properties) { - shouldReinstall = true - } - } else { - if planData.Os.ValueString() != "" { - shouldReinstall = true - } +func (r *dedicatedServerResource) reinstallDedicatedServer(ctx context.Context, preventReinstall bool, stateData, planData *DedicatedServerModel) error { + tflog.Debug(ctx, fmt.Sprintf("Prevent server reinstallation: %t", preventReinstall)) + tflog.Debug(ctx, fmt.Sprintf("State data is nil: %t", stateData == nil)) + + if preventReinstall { + tflog.Debug(ctx, "Prevent reinstallation of the server is true") + return nil } // Get service name @@ -301,28 +376,55 @@ func (r *dedicatedServerResource) updateDedicatedServerResource(ctx context.Cont serviceName = stateData.ServiceName.ValueString() } + var shouldReinstall bool + + switch stateData { + // Check if we should install server right after it has been delivered + case nil: + if planData.Os.ValueString() != "" { + shouldReinstall = true + } + + // Classical update when resource already exists. + // Checks state data against plan data to decide if a reinstall should be triggered. + default: + if planData.Os.ValueString() != "" && + stateData.Os.ValueString() != planData.Os.ValueString() || + !stateData.Customizations.Equal(planData.Customizations) || + !stateData.Storage.Equal(planData.Storage) || + !stateData.Properties.Equal(planData.Properties) { + shouldReinstall = true + } + } + // Trigger server reinstallation - endpoint := "/dedicated/server/" + url.PathEscape(serviceName) + "/reinstall" if shouldReinstall { log.Print("Triggering server reinstallation") + task := DedicatedServerTask{} + endpoint := "/dedicated/server/" + url.PathEscape(serviceName) + "/reinstall" if err := r.config.OVHClient.Post(endpoint, planData.ToReinstall(), &task); err != nil { - diags.AddError( - fmt.Sprintf("Error calling Post %s", endpoint), - err.Error(), - ) - return + return fmt.Errorf("error calling Post %s", endpoint) } // Wait for reinstallation completion if err := waitForDedicatedServerTask(serviceName, &task, r.config.OVHClient); err != nil { - diags.AddError("Error during server reinstallation", err.Error()) - return + return fmt.Errorf("error during server reinstallation: %s", err.Error()) } } + return nil +} + +func (r *dedicatedServerResource) updateDedicatedServerResource(ctx context.Context, stateData, planData, responseData *DedicatedServerModel, diags *diag.Diagnostics) { + // Get service name + serviceName := planData.ServiceName.ValueString() + if stateData != nil { + serviceName = stateData.ServiceName.ValueString() + } + // PUT the resource - endpoint = "/dedicated/server/" + url.PathEscape(serviceName) + endpoint := "/dedicated/server/" + url.PathEscape(serviceName) if err := r.config.OVHClient.Put(endpoint, planData.ToUpdate(), nil); err != nil { diags.AddError( fmt.Sprintf("Error calling Put %s", endpoint), diff --git a/ovh/resource_dedicated_server_gen.go b/ovh/resource_dedicated_server_gen.go index 946fd581c..c0c792b0e 100644 --- a/ovh/resource_dedicated_server_gen.go +++ b/ovh/resource_dedicated_server_gen.go @@ -6,18 +6,22 @@ import ( "context" "encoding/json" "fmt" + "strings" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-go/tftypes" ovhtypes "github.com/ovh/terraform-provider-ovh/v2/ovh/types" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/resource/schema" ) func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { @@ -27,11 +31,17 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { Computed: true, Description: "dedicated AZ localisation", MarkdownDescription: "dedicated AZ localisation", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "boot_id": schema.Int64Attribute{ CustomType: ovhtypes.TfInt64Type{}, Optional: true, Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, "boot_script": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, @@ -39,12 +49,18 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { Computed: true, Description: "Ipxe script served on boot", MarkdownDescription: "Ipxe script served on boot", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "commercial_range": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Computed: true, Description: "dedicater server commercial range", MarkdownDescription: "dedicater server commercial range", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "customizations": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ @@ -168,68 +184,12 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { Computed: true, Description: "dedicated datacenter localisation", MarkdownDescription: "dedicated datacenter localisation", - Validators: []validator.String{ - stringvalidator.OneOf( - "bhs1", - "bhs2", - "bhs3", - "bhs4", - "bhs5", - "bhs6", - "bhs7", - "bhs8", - "cch01", - "crx1", - "crx2", - "dc1", - "eri1", - "eri2", - "gra04", - "gra1", - "gra2", - "gra3", - "gsw", - "hdf01", - "hil1", - "ieb01", - "lil1-int1", - "lim1", - "lim2", - "lim3", - "mr901", - "p19", - "rbx", - "rbx-hz", - "rbx1", - "rbx10", - "rbx2", - "rbx3", - "rbx4", - "rbx5", - "rbx6", - "rbx7", - "rbx8", - "rbx9", - "sbg1", - "sbg2", - "sbg3", - "sbg4", - "sbg5", - "sgp02", - "sgp1", - "syd03", - "syd1", - "syd2", - "vin1", - "waw1", - "ynm1", - "yyz01", - ), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, }, "display_name": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, - Computed: true, Optional: true, Description: "The display name of your dedicated server", MarkdownDescription: "The display name of your dedicated server", @@ -240,6 +200,9 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { Computed: true, Description: "Path of the EFI bootloader served on boot", MarkdownDescription: "Path of the EFI bootloader served on boot", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "iam": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ @@ -282,10 +245,22 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { Computed: true, Description: "dedicated server ip", MarkdownDescription: "dedicated server ip", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "keep_service_after_destroy": schema.BoolAttribute{ + CustomType: ovhtypes.TfBoolType{}, + Optional: true, + Description: "Whether we should avoid terminating the service when destroying the resource", + MarkdownDescription: "Whether we should avoid terminating the service when destroying the resource", }, "link_speed": schema.Int64Attribute{ CustomType: ovhtypes.TfInt64Type{}, Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, "monitoring": schema.BoolAttribute{ CustomType: ovhtypes.TfBoolType{}, @@ -293,16 +268,25 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { Computed: true, Description: "Icmp monitoring state", MarkdownDescription: "Icmp monitoring state", + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "name": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Computed: true, Description: "dedicated server name", MarkdownDescription: "dedicated server name", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "new_upgrade_system": schema.BoolAttribute{ CustomType: ovhtypes.TfBoolType{}, Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "no_intervention": schema.BoolAttribute{ CustomType: ovhtypes.TfBoolType{}, @@ -310,6 +294,9 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { Computed: true, Description: "Prevent datacenter intervention", MarkdownDescription: "Prevent datacenter intervention", + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "os": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, @@ -329,12 +316,18 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { "poweron", ), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "professional_use": schema.BoolAttribute{ CustomType: ovhtypes.TfBoolType{}, Computed: true, Description: "Does this server have professional use option", MarkdownDescription: "Does this server have professional use option", + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "properties": schema.MapAttribute{ CustomType: ovhtypes.NewTfMapNestedType[ovhtypes.TfStringValue](ctx), @@ -345,49 +338,84 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { "rack": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "prevent_install_on_create": schema.BoolAttribute{ + CustomType: ovhtypes.TfBoolType{}, + Optional: true, + Description: "Defines whether the server should not be reinstalled after creating the resource", + MarkdownDescription: "Defines whether the server should not be reinstalled after creating the resource", + }, + "prevent_install_on_import": schema.BoolAttribute{ + CustomType: ovhtypes.TfBoolType{}, + Optional: true, + Description: "Defines whether the server should not be reinstalled when importing the resource", + MarkdownDescription: "Defines whether the server should not be reinstalled when importing the resource", }, "region": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Computed: true, Description: "dedicated region localisation", MarkdownDescription: "dedicated region localisation", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "rescue_mail": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Optional: true, - Computed: true, Description: "Custom email used to receive rescue credentials", MarkdownDescription: "Custom email used to receive rescue credentials", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "rescue_ssh_key": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Optional: true, - Computed: true, Description: "Public SSH Key used in the rescue mode", MarkdownDescription: "Public SSH Key used in the rescue mode", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "reverse": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Computed: true, Description: "dedicated server reverse", MarkdownDescription: "dedicated server reverse", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "root_device": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Optional: true, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "server_id": schema.Int64Attribute{ CustomType: ovhtypes.TfInt64Type{}, Computed: true, Description: "Server id", MarkdownDescription: "Server id", + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, "service_name": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, Computed: true, + Optional: true, Description: "The internal name of your dedicated server", MarkdownDescription: "The internal name of your dedicated server", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "state": schema.StringAttribute{ CustomType: ovhtypes.TfStringType{}, @@ -403,6 +431,9 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { "ok", ), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "storage": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ @@ -627,6 +658,9 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { "pro", ), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, } for k, v := range OrderResourceSchema(ctx).Attributes { @@ -639,40 +673,43 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema { } type DedicatedServerModel struct { - AvailabilityZone ovhtypes.TfStringValue `tfsdk:"availability_zone" json:"availabilityZone"` - BootId ovhtypes.TfInt64Value `tfsdk:"boot_id" json:"bootId"` - BootScript ovhtypes.TfStringValue `tfsdk:"boot_script" json:"bootScript"` - CommercialRange ovhtypes.TfStringValue `tfsdk:"commercial_range" json:"commercialRange"` - Customizations CustomizationsValue `tfsdk:"customizations" json:"customizations"` - Datacenter ovhtypes.TfStringValue `tfsdk:"datacenter" json:"datacenter"` - DisplayName ovhtypes.TfStringValue `tfsdk:"display_name" json:"displayName"` - EfiBootloaderPath ovhtypes.TfStringValue `tfsdk:"efi_bootloader_path" json:"efiBootloaderPath"` - Iam IamValue `tfsdk:"iam" json:"iam"` - Ip ovhtypes.TfStringValue `tfsdk:"ip" json:"ip"` - LinkSpeed ovhtypes.TfInt64Value `tfsdk:"link_speed" json:"linkSpeed"` - Monitoring ovhtypes.TfBoolValue `tfsdk:"monitoring" json:"monitoring"` - Name ovhtypes.TfStringValue `tfsdk:"name" json:"name"` - NewUpgradeSystem ovhtypes.TfBoolValue `tfsdk:"new_upgrade_system" json:"newUpgradeSystem"` - NoIntervention ovhtypes.TfBoolValue `tfsdk:"no_intervention" json:"noIntervention"` - Os ovhtypes.TfStringValue `tfsdk:"os" json:"os"` - PowerState ovhtypes.TfStringValue `tfsdk:"power_state" json:"powerState"` - ProfessionalUse ovhtypes.TfBoolValue `tfsdk:"professional_use" json:"professionalUse"` - Properties ovhtypes.TfMapNestedValue[ovhtypes.TfStringValue] `tfsdk:"properties" json:"properties"` - Rack ovhtypes.TfStringValue `tfsdk:"rack" json:"rack"` - Region ovhtypes.TfStringValue `tfsdk:"region" json:"region"` - RescueMail ovhtypes.TfStringValue `tfsdk:"rescue_mail" json:"rescueMail"` - RescueSshKey ovhtypes.TfStringValue `tfsdk:"rescue_ssh_key" json:"rescueSshKey"` - Reverse ovhtypes.TfStringValue `tfsdk:"reverse" json:"reverse"` - RootDevice ovhtypes.TfStringValue `tfsdk:"root_device" json:"rootDevice"` - ServerId ovhtypes.TfInt64Value `tfsdk:"server_id" json:"serverId"` - ServiceName ovhtypes.TfStringValue `tfsdk:"service_name" json:"serviceName"` - State ovhtypes.TfStringValue `tfsdk:"state" json:"state"` - Storage ovhtypes.TfListNestedValue[StorageValue] `tfsdk:"storage" json:"storage"` - SupportLevel ovhtypes.TfStringValue `tfsdk:"support_level" json:"supportLevel"` - Order OrderValue `tfsdk:"order" json:"order"` - OvhSubsidiary ovhtypes.TfStringValue `tfsdk:"ovh_subsidiary" json:"ovhSubsidiary"` - Plan ovhtypes.TfListNestedValue[PlanValue] `tfsdk:"plan" json:"plan"` - PlanOption ovhtypes.TfListNestedValue[PlanOptionValue] `tfsdk:"plan_option" json:"planOption"` + AvailabilityZone ovhtypes.TfStringValue `tfsdk:"availability_zone" json:"availabilityZone"` + BootId ovhtypes.TfInt64Value `tfsdk:"boot_id" json:"bootId"` + BootScript ovhtypes.TfStringValue `tfsdk:"boot_script" json:"bootScript"` + CommercialRange ovhtypes.TfStringValue `tfsdk:"commercial_range" json:"commercialRange"` + Customizations CustomizationsValue `tfsdk:"customizations" json:"customizations"` + Datacenter ovhtypes.TfStringValue `tfsdk:"datacenter" json:"datacenter"` + DisplayName ovhtypes.TfStringValue `tfsdk:"display_name" json:"displayName"` + EfiBootloaderPath ovhtypes.TfStringValue `tfsdk:"efi_bootloader_path" json:"efiBootloaderPath"` + Iam IamValue `tfsdk:"iam" json:"iam"` + Ip ovhtypes.TfStringValue `tfsdk:"ip" json:"ip"` + LinkSpeed ovhtypes.TfInt64Value `tfsdk:"link_speed" json:"linkSpeed"` + Monitoring ovhtypes.TfBoolValue `tfsdk:"monitoring" json:"monitoring"` + Name ovhtypes.TfStringValue `tfsdk:"name" json:"name"` + NewUpgradeSystem ovhtypes.TfBoolValue `tfsdk:"new_upgrade_system" json:"newUpgradeSystem"` + NoIntervention ovhtypes.TfBoolValue `tfsdk:"no_intervention" json:"noIntervention"` + Os ovhtypes.TfStringValue `tfsdk:"os" json:"os"` + PowerState ovhtypes.TfStringValue `tfsdk:"power_state" json:"powerState"` + ProfessionalUse ovhtypes.TfBoolValue `tfsdk:"professional_use" json:"professionalUse"` + Properties ovhtypes.TfMapNestedValue[ovhtypes.TfStringValue] `tfsdk:"properties" json:"properties"` + Rack ovhtypes.TfStringValue `tfsdk:"rack" json:"rack"` + PreventInstallOnCreate ovhtypes.TfBoolValue `tfsdk:"prevent_install_on_create" json:"-"` + PreventInstallOnImport ovhtypes.TfBoolValue `tfsdk:"prevent_install_on_import" json:"-"` + Region ovhtypes.TfStringValue `tfsdk:"region" json:"region"` + RescueMail ovhtypes.TfStringValue `tfsdk:"rescue_mail" json:"rescueMail"` + RescueSshKey ovhtypes.TfStringValue `tfsdk:"rescue_ssh_key" json:"rescueSshKey"` + Reverse ovhtypes.TfStringValue `tfsdk:"reverse" json:"reverse"` + RootDevice ovhtypes.TfStringValue `tfsdk:"root_device" json:"rootDevice"` + ServerId ovhtypes.TfInt64Value `tfsdk:"server_id" json:"serverId"` + ServiceName ovhtypes.TfStringValue `tfsdk:"service_name" json:"serviceName"` + State ovhtypes.TfStringValue `tfsdk:"state" json:"state"` + SupportLevel ovhtypes.TfStringValue `tfsdk:"support_level" json:"supportLevel"` + Storage ovhtypes.TfListNestedValue[StorageValue] `tfsdk:"storage" json:"storage"` + KeepServiceAfterDestroy ovhtypes.TfBoolValue `tfsdk:"keep_service_after_destroy" json:"-"` + Order OrderValue `tfsdk:"order" json:"order"` + OvhSubsidiary ovhtypes.TfStringValue `tfsdk:"ovh_subsidiary" json:"ovhSubsidiary"` + Plan ovhtypes.TfListNestedValue[PlanValue] `tfsdk:"plan" json:"plan"` + PlanOption ovhtypes.TfListNestedValue[PlanOptionValue] `tfsdk:"plan_option" json:"planOption"` } func (v *DedicatedServerModel) MergeWith(other *DedicatedServerModel) { @@ -744,6 +781,10 @@ func (v *DedicatedServerModel) MergeWith(other *DedicatedServerModel) { v.Os = other.Os } + if (v.Properties.IsUnknown() || v.Properties.IsNull()) && !other.Properties.IsUnknown() { + v.Properties = other.Properties + } + if (v.PowerState.IsUnknown() || v.PowerState.IsNull()) && !other.PowerState.IsUnknown() { v.PowerState = other.PowerState } @@ -760,6 +801,14 @@ func (v *DedicatedServerModel) MergeWith(other *DedicatedServerModel) { v.Rack = other.Rack } + if (v.PreventInstallOnCreate.IsUnknown() || v.PreventInstallOnCreate.IsNull()) && !other.PreventInstallOnCreate.IsUnknown() { + v.PreventInstallOnCreate = other.PreventInstallOnCreate + } + + if (v.PreventInstallOnImport.IsUnknown() || v.PreventInstallOnImport.IsNull()) && !other.PreventInstallOnImport.IsUnknown() { + v.PreventInstallOnImport = other.PreventInstallOnImport + } + if (v.Region.IsUnknown() || v.Region.IsNull()) && !other.Region.IsUnknown() { v.Region = other.Region } @@ -768,7 +817,7 @@ func (v *DedicatedServerModel) MergeWith(other *DedicatedServerModel) { v.RescueMail = other.RescueMail } - if (v.RescueSshKey.IsUnknown() || v.RescueSshKey.IsNull()) && !other.RescueSshKey.IsUnknown() { + if v.RescueSshKey.IsUnknown() && !other.RescueSshKey.IsUnknown() { v.RescueSshKey = other.RescueSshKey } @@ -800,6 +849,10 @@ func (v *DedicatedServerModel) MergeWith(other *DedicatedServerModel) { v.SupportLevel = other.SupportLevel } + if (v.KeepServiceAfterDestroy.IsUnknown() || v.KeepServiceAfterDestroy.IsNull()) && !other.KeepServiceAfterDestroy.IsUnknown() { + v.KeepServiceAfterDestroy = other.KeepServiceAfterDestroy + } + if (v.Order.IsUnknown() || v.Order.IsNull()) && !other.Order.IsUnknown() { v.Order = other.Order } @@ -857,7 +910,7 @@ type DedicatedServerWritableModel struct { RescueSshKey *ovhtypes.TfStringValue `tfsdk:"rescue_ssh_key" json:"rescueSshKey,omitempty"` RootDevice *ovhtypes.TfStringValue `tfsdk:"root_device" json:"rootDevice,omitempty"` State *ovhtypes.TfStringValue `tfsdk:"state" json:"state,omitempty"` - Storage *ovhtypes.TfListNestedValue[StorageWritableValue] `tfsdk:"storage" json:"storage,omitempty"` + Storage []*StorageWritableValue `tfsdk:"storage" json:"storage,omitempty"` Os *ovhtypes.TfStringValue `tfsdk:"os" json:"operatingSystem,omitempty"` } @@ -911,20 +964,12 @@ func (v DedicatedServerModel) ToReinstall() *DedicatedServerWritableModel { } if !v.Customizations.IsUnknown() && !v.Customizations.IsNull() { - res.Customizations = v.Customizations.ToUpdate() + res.Customizations = v.Customizations.ToCreate() } if !v.Storage.IsUnknown() && !v.Storage.IsNull() { - var updateStorage []*StorageWritableValue for _, elem := range v.Storage.Elements() { - elemToUpdate := elem.(StorageValue).ToUpdate() - updateStorage = append(updateStorage, elemToUpdate) - } - - newStorage, _ := basetypes.NewListValueFrom(context.Background(), - StorageWritableValue{}.Type(context.Background()), updateStorage) - res.Storage = &ovhtypes.TfListNestedValue[StorageWritableValue]{ - ListValue: newStorage, + res.Storage = append(res.Storage, elem.(StorageValue).ToCreate()) } } @@ -937,28 +982,35 @@ func (v DedicatedServerModel) ToReinstall() *DedicatedServerWritableModel { func (v DedicatedServerModel) ToUpdate() *DedicatedServerWritableModel { res := &DedicatedServerWritableModel{} + emptyString := ovhtypes.NewTfStringValue("") if !v.BootId.IsUnknown() { res.BootId = &v.BootId } - if !v.BootScript.IsUnknown() { + if v.BootScript.IsNull() { + res.BootScript = &emptyString + } else if !v.BootScript.IsUnknown() { res.BootScript = &v.BootScript } - if !v.EfiBootloaderPath.IsUnknown() { + if v.EfiBootloaderPath.IsNull() { + res.EfiBootloaderPath = &emptyString + } else if !v.EfiBootloaderPath.IsUnknown() { res.EfiBootloaderPath = &v.EfiBootloaderPath } - if !v.Monitoring.IsUnknown() { + if !v.Monitoring.IsUnknown() && !v.Monitoring.IsNull() { res.Monitoring = &v.Monitoring } - if !v.NoIntervention.IsUnknown() { + if !v.NoIntervention.IsUnknown() && !v.NoIntervention.IsNull() { res.NoIntervention = &v.NoIntervention } - if !v.RescueMail.IsUnknown() { + if v.RescueMail.IsNull() { + res.RescueMail = &emptyString + } else if !v.RescueMail.IsUnknown() { res.RescueMail = &v.RescueMail } @@ -966,11 +1018,13 @@ func (v DedicatedServerModel) ToUpdate() *DedicatedServerWritableModel { res.RescueSshKey = &v.RescueSshKey } - if !v.RootDevice.IsUnknown() { + if v.RootDevice.IsNull() { + res.RootDevice = &emptyString + } else if !v.RootDevice.IsUnknown() { res.RootDevice = &v.RootDevice } - if !v.State.IsUnknown() { + if !v.State.IsUnknown() && !v.State.IsNull() { res.State = &v.State } @@ -1614,7 +1668,7 @@ type CustomizationsValue struct { ImageCheckSum ovhtypes.TfStringValue `tfsdk:"image_check_sum" json:"imageCheckSum"` ImageCheckSumType ovhtypes.TfStringValue `tfsdk:"image_check_sum_type" json:"imageCheckSumType"` ImageType ovhtypes.TfStringValue `tfsdk:"image_type" json:"imageType"` - ImageUrl ovhtypes.TfStringValue `tfsdk:"image_url" json:"imageUrl"` + ImageUrl ovhtypes.TfStringValue `tfsdk:"image_url" json:"imageURL"` Language ovhtypes.TfStringValue `tfsdk:"language" json:"language"` PostInstallationScript ovhtypes.TfStringValue `tfsdk:"post_installation_script" json:"postInstallationScript"` PostInstallationScriptExtension ovhtypes.TfStringValue `tfsdk:"post_installation_script_extension" json:"postInstallationScriptExtension"` @@ -1623,124 +1677,71 @@ type CustomizationsValue struct { } type CustomizationsWritableValue struct { - *CustomizationsValue `json:"-"` - CustomHostname *ovhtypes.TfStringValue `json:"customHostname,omitempty"` - DiskGroupId *ovhtypes.TfInt64Value `json:"diskGroupId,omitempty"` - NoRaid *ovhtypes.TfBoolValue `json:"noRaid,omitempty"` - SoftRaidDevices *ovhtypes.TfInt64Value `json:"softRaidDevices,omitempty"` + ConfigDriveUserData *ovhtypes.TfStringValue `json:"configDriveUserData,omitempty"` + EfiBootloaderPath *ovhtypes.TfStringValue `json:"efiBootloaderPath,omitempty"` + Hostname *ovhtypes.TfStringValue `json:"hostname,omitempty"` + HttpHeaders *ovhtypes.TfMapNestedValue[ovhtypes.TfStringValue] `json:"httpHeaders,omitempty"` + ImageCheckSum *ovhtypes.TfStringValue `json:"imageCheckSum,omitempty"` + ImageCheckSumType *ovhtypes.TfStringValue `json:"imageCheckSumType,omitempty"` + ImageType *ovhtypes.TfStringValue `json:"imageType,omitempty"` + ImageUrl *ovhtypes.TfStringValue `json:"imageURL,omitempty"` + Language *ovhtypes.TfStringValue `json:"language,omitempty"` + PostInstallationScript *ovhtypes.TfStringValue `json:"postInstallationScript,omitempty"` + PostInstallationScriptExtension *ovhtypes.TfStringValue `json:"postInstallationScriptExtension,omitempty"` + SshKey *ovhtypes.TfStringValue `json:"sshKey,omitempty"` } func (v CustomizationsValue) ToCreate() *CustomizationsWritableValue { res := &CustomizationsWritableValue{} if !v.ImageType.IsNull() { - res.ImageType = v.ImageType - } - - if !v.Language.IsNull() { - res.Language = v.Language - } - - if !v.PostInstallationScript.IsNull() { - res.PostInstallationScript = v.PostInstallationScript - } - - if !v.PostInstallationScriptExtension.IsNull() { - res.PostInstallationScriptExtension = v.PostInstallationScriptExtension - } - - if !v.Hostname.IsNull() { - res.Hostname = v.Hostname - } - - if !v.ImageCheckSum.IsNull() { - res.ImageCheckSum = v.ImageCheckSum - } - - if !v.SshKey.IsNull() { - res.SshKey = v.SshKey - } - - if !v.EfiBootloaderPath.IsNull() { - res.EfiBootloaderPath = v.EfiBootloaderPath - } - - if !v.HttpHeaders.IsNull() { - res.HttpHeaders = v.HttpHeaders - } - - if !v.ImageCheckSumType.IsNull() { - res.ImageCheckSumType = v.ImageCheckSumType - } - - if !v.ImageUrl.IsNull() { - res.ImageUrl = v.ImageUrl - } - - if !v.ConfigDriveUserData.IsNull() { - res.ConfigDriveUserData = v.ConfigDriveUserData - } - - res.state = attr.ValueStateKnown - - return res -} - -func (v CustomizationsValue) ToUpdate() *CustomizationsWritableValue { - res := &CustomizationsWritableValue{ - CustomizationsValue: &CustomizationsValue{}, - } - - if !v.ImageType.IsNull() { - res.ImageType = v.ImageType + res.ImageType = &v.ImageType } if !v.Language.IsNull() { - res.Language = v.Language + res.Language = &v.Language } if !v.PostInstallationScript.IsNull() { - res.PostInstallationScript = v.PostInstallationScript + res.PostInstallationScript = &v.PostInstallationScript } if !v.PostInstallationScriptExtension.IsNull() { - res.PostInstallationScriptExtension = v.PostInstallationScriptExtension + res.PostInstallationScriptExtension = &v.PostInstallationScriptExtension } if !v.Hostname.IsNull() { - res.Hostname = v.Hostname + res.Hostname = &v.Hostname } if !v.ImageCheckSum.IsNull() { - res.ImageCheckSum = v.ImageCheckSum + res.ImageCheckSum = &v.ImageCheckSum } if !v.SshKey.IsNull() { - res.SshKey = v.SshKey + res.SshKey = &v.SshKey } if !v.EfiBootloaderPath.IsNull() { - res.EfiBootloaderPath = v.EfiBootloaderPath + res.EfiBootloaderPath = &v.EfiBootloaderPath } if !v.HttpHeaders.IsNull() { - res.HttpHeaders = v.HttpHeaders + res.HttpHeaders = &v.HttpHeaders } if !v.ImageCheckSumType.IsNull() { - res.ImageCheckSumType = v.ImageCheckSumType + res.ImageCheckSumType = &v.ImageCheckSumType } if !v.ImageUrl.IsNull() { - res.ImageUrl = v.ImageUrl + res.ImageUrl = &v.ImageUrl } if !v.ConfigDriveUserData.IsNull() { - res.ConfigDriveUserData = v.ConfigDriveUserData + res.ConfigDriveUserData = &v.ConfigDriveUserData } - res.state = attr.ValueStateKnown - return res } @@ -2207,139 +2208,6 @@ func (t StorageType) ValueFromObject(ctx context.Context, in basetypes.ObjectVal }, diags } -type StorageWritableType struct { - basetypes.ObjectType -} - -func (t StorageWritableType) Equal(o attr.Type) bool { - other, ok := o.(StorageWritableType) - - if !ok { - return false - } - - return t.ObjectType.Equal(other.ObjectType) -} - -func (t StorageWritableType) String() string { - return "StorageWritableType" -} - -func (t StorageWritableType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { - var diags diag.Diagnostics - - attributes := in.Attributes() - - diskGroupIdAttribute, ok := attributes["disk_group_id"] - - if !ok { - diags.AddError( - "Attribute Missing", - `disk_group_id is missing from object`) - - return nil, diags - } - - diskGroupIdVal, ok := diskGroupIdAttribute.(ovhtypes.TfInt64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`disk_group_id expected to be ovhtypes.TfInt64Value, was: %T`, diskGroupIdAttribute)) - } - - hardwareRaidAttribute, ok := attributes["hardware_raid"] - - if !ok { - diags.AddError( - "Attribute Missing", - `hardware_raid is missing from object`) - - return nil, diags - } - - hardwareRaidVal, ok := hardwareRaidAttribute.(ovhtypes.TfListNestedValue[StorageHardwareRaidValue]) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`hardware_raid expected to be ovhtypes.TfListNestedValue[StorageHardwareRaidValue], was: %T`, hardwareRaidAttribute)) - } - - partitioningAttribute, ok := attributes["partitioning"] - - if !ok { - diags.AddError( - "Attribute Missing", - `partitioning is missing from object`) - - return nil, diags - } - - partitioningVal, ok := partitioningAttribute.(StoragePartitioningValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`partitioning expected to be StoragePartitioningValue, was: %T`, partitioningAttribute)) - } - - if diags.HasError() { - return nil, diags - } - - return StorageWritableValue{ - DiskGroupId: &diskGroupIdVal, - HardwareRaid: &hardwareRaidVal, - Partitioning: &partitioningVal, - state: attr.ValueStateKnown, - }, diags -} - -func (t StorageWritableType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { - if in.Type() == nil { - return NewStorageValueNull(), nil - } - - if !in.Type().Equal(t.TerraformType(ctx)) { - return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) - } - - if !in.IsKnown() { - return NewStorageValueUnknown(), nil - } - - if in.IsNull() { - return NewStorageValueNull(), nil - } - - attributes := map[string]attr.Value{} - - val := map[string]tftypes.Value{} - - err := in.As(&val) - - if err != nil { - return nil, err - } - - for k, v := range val { - a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) - - if err != nil { - return nil, err - } - - attributes[k] = a - } - - return NewStorageValueMust(StorageValue{}.AttributeTypes(ctx), attributes), nil -} - -func (t StorageWritableType) ValueType(ctx context.Context) attr.Value { - return StorageWritableValue{} -} - func NewStorageValueNull() StorageValue { return StorageValue{ state: attr.ValueStateNull, @@ -2490,144 +2358,6 @@ func NewStorageValueMust(attributeTypes map[string]attr.Type, attributes map[str return object } -func NewStorageWritableValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (StorageValue, diag.Diagnostics) { - var diags diag.Diagnostics - - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 - ctx := context.Background() - - for name, attributeType := range attributeTypes { - attribute, ok := attributes[name] - - if !ok { - diags.AddError( - "Missing StorageValue Attribute Value", - "While creating a StorageValue value, a missing attribute value was detected. "+ - "A StorageValue must contain values for all attributes, even if null or unknown. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("StorageValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), - ) - - continue - } - - if !attributeType.Equal(attribute.Type(ctx)) { - diags.AddError( - "Invalid StorageValue Attribute Type", - "While creating a StorageValue value, an invalid attribute value was detected. "+ - "A StorageValue must use a matching attribute type for the value. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("StorageValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ - fmt.Sprintf("StorageValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), - ) - } - } - - for name := range attributes { - _, ok := attributeTypes[name] - - if !ok { - diags.AddError( - "Extra StorageValue Attribute Value", - "While creating a StorageValue value, an extra attribute value was detected. "+ - "A StorageValue must not contain values beyond the expected attribute types. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("Extra StorageValue Attribute Name: %s", name), - ) - } - } - - if diags.HasError() { - return NewStorageValueUnknown(), diags - } - - diskGroupIdAttribute, ok := attributes["disk_group_id"] - - if !ok { - diags.AddError( - "Attribute Missing", - `disk_group_id is missing from object`) - - return NewStorageValueUnknown(), diags - } - - diskGroupIdVal, ok := diskGroupIdAttribute.(ovhtypes.TfInt64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`disk_group_id expected to be ovhtypes.TfInt64Value, was: %T`, diskGroupIdAttribute)) - } - - hardwareRaidAttribute, ok := attributes["hardware_raid"] - - if !ok { - diags.AddError( - "Attribute Missing", - `hardware_raid is missing from object`) - - return NewStorageValueUnknown(), diags - } - - hardwareRaidVal, ok := hardwareRaidAttribute.(ovhtypes.TfListNestedValue[StorageHardwareRaidValue]) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`hardware_raid expected to be ovhtypes.TfListNestedValue[StorageHardwareRaidValue], was: %T`, hardwareRaidAttribute)) - } - - partitioningAttribute, ok := attributes["partitioning"] - - if !ok { - diags.AddError( - "Attribute Missing", - `partitioning is missing from object`) - - return NewStorageValueUnknown(), diags - } - - partitioningVal, ok := partitioningAttribute.(StoragePartitioningValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`partitioning expected to be StoragePartitioningValue, was: %T`, partitioningAttribute)) - } - - if diags.HasError() { - return NewStorageValueUnknown(), diags - } - - return StorageValue{ - DiskGroupId: diskGroupIdVal, - HardwareRaid: hardwareRaidVal, - Partitioning: partitioningVal, - state: attr.ValueStateKnown, - }, diags -} - -func NewStorageValueWritableMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) StorageValue { - object, diags := NewStorageValue(attributeTypes, attributes) - - if diags.HasError() { - // This could potentially be added to the diag package. - diagsStrings := make([]string, 0, len(diags)) - - for _, diagnostic := range diags { - diagsStrings = append(diagsStrings, fmt.Sprintf( - "%s | %s | %s", - diagnostic.Severity(), - diagnostic.Summary(), - diagnostic.Detail())) - } - - panic("NewStorageValueWritableMust received error(s): " + strings.Join(diagsStrings, "\n")) - } - - return object -} - func (t StorageType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { if in.Type() == nil { return NewStorageValueNull(), nil @@ -2672,63 +2402,38 @@ func (t StorageType) ValueType(ctx context.Context) attr.Value { return StorageValue{} } -var _ basetypes.ObjectValuable = StorageValue{} - -type StorageValue struct { - DiskGroupId ovhtypes.TfInt64Value `tfsdk:"disk_group_id" json:"diskGroupId"` - HardwareRaid ovhtypes.TfListNestedValue[StorageHardwareRaidValue] `tfsdk:"hardware_raid" json:"hardwareRaid"` - Partitioning StoragePartitioningValue `tfsdk:"partitioning" json:"partitioning"` - state attr.ValueState -} - -type StorageWritableValue struct { - DiskGroupId *ovhtypes.TfInt64Value `tfsdk:"disk_group_id" json:"diskGroupId,omitempty"` - HardwareRaid *ovhtypes.TfListNestedValue[StorageHardwareRaidValue] `tfsdk:"hardware_raid" json:"hardwareRaid,omitempty"` - Partitioning *StoragePartitioningValue `tfsdk:"partitioning" json:"partitioning,omitempty"` - state attr.ValueState -} - -func (v StorageValue) ToCreate() *StorageWritableValue { - res := &StorageWritableValue{ - state: v.state, - } - - if !v.DiskGroupId.IsNull() { - res.DiskGroupId = &v.DiskGroupId - } - - if !v.HardwareRaid.IsNull() { - res.HardwareRaid = &v.HardwareRaid - } - - if !v.Partitioning.IsNull() { - res.Partitioning = &v.Partitioning - } +var _ basetypes.ObjectValuable = StorageValue{} - res.state = attr.ValueStateKnown +type StorageValue struct { + DiskGroupId ovhtypes.TfInt64Value `tfsdk:"disk_group_id" json:"diskGroupId"` + HardwareRaid ovhtypes.TfListNestedValue[StorageHardwareRaidValue] `tfsdk:"hardware_raid" json:"hardwareRaid"` + Partitioning StoragePartitioningValue `tfsdk:"partitioning" json:"partitioning"` + state attr.ValueState +} - return res +type StorageWritableValue struct { + DiskGroupId *ovhtypes.TfInt64Value `json:"diskGroupId,omitempty"` + HardwareRaid []*StorageHardwareRaidValue `json:"hardwareRaid,omitempty"` + Partitioning *StoragePartitioningValue `json:"partitioning,omitempty"` } -func (v StorageValue) ToUpdate() *StorageWritableValue { - res := &StorageWritableValue{ - state: v.state, - } +func (v StorageValue) ToCreate() *StorageWritableValue { + res := &StorageWritableValue{} if !v.DiskGroupId.IsNull() { res.DiskGroupId = &v.DiskGroupId } if !v.HardwareRaid.IsNull() { - res.HardwareRaid = &v.HardwareRaid + for _, elem := range v.HardwareRaid.Elements() { + res.HardwareRaid = append(res.HardwareRaid, elem.(StorageHardwareRaidValue).ToCreate()) + } } if !v.Partitioning.IsNull() { - res.Partitioning = &v.Partitioning + res.Partitioning = v.Partitioning.ToCreate() } - res.state = attr.ValueStateKnown - return res } @@ -2948,152 +2653,6 @@ func (v StorageValue) AttributeTypes(ctx context.Context) map[string]attr.Type { } } -func (v StorageWritableValue) Type(ctx context.Context) attr.Type { - return StorageType{ - basetypes.ObjectType{ - AttrTypes: v.AttributeTypes(ctx), - }, - } -} - -func (v StorageWritableValue) AttributeTypes(ctx context.Context) map[string]attr.Type { - return map[string]attr.Type{ - "disk_group_id": ovhtypes.TfInt64Type{}, - "hardware_raid": ovhtypes.NewTfListNestedType[StorageHardwareRaidValue](ctx), - "partitioning": StoragePartitioningValue{}.Type(ctx), - } -} - -func (v StorageWritableValue) Attributes() map[string]attr.Value { - return map[string]attr.Value{ - "disk_group_id": v.DiskGroupId, - "hardware_raid": v.HardwareRaid, - "partitioning": v.Partitioning, - } -} - -func (v StorageWritableValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - attrTypes := make(map[string]tftypes.Type, 3) - - var val tftypes.Value - var err error - - attrTypes["disk_group_id"] = basetypes.Int64Type{}.TerraformType(ctx) - attrTypes["hardware_raid"] = basetypes.ListType{ - ElemType: StorageHardwareRaidValue{}.Type(ctx), - }.TerraformType(ctx) - attrTypes["partitioning"] = basetypes.ObjectType{ - AttrTypes: StoragePartitioningValue{}.AttributeTypes(ctx), - }.TerraformType(ctx) - - objectType := tftypes.Object{AttributeTypes: attrTypes} - - switch v.state { - case attr.ValueStateKnown: - vals := make(map[string]tftypes.Value, 3) - - val, err = v.DiskGroupId.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["disk_group_id"] = val - - val, err = v.HardwareRaid.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["hardware_raid"] = val - - val, err = v.Partitioning.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["partitioning"] = val - - if err := tftypes.ValidateValue(objectType, vals); err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - return tftypes.NewValue(objectType, vals), nil - case attr.ValueStateNull: - return tftypes.NewValue(objectType, nil), nil - case attr.ValueStateUnknown: - return tftypes.NewValue(objectType, tftypes.UnknownValue), nil - default: - panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) - } -} - -func (v StorageWritableValue) IsNull() bool { - return v.state == attr.ValueStateNull -} - -func (v StorageWritableValue) IsUnknown() bool { - return v.state == attr.ValueStateUnknown -} - -func (v StorageWritableValue) String() string { - return "StorageValue" -} - -func (v StorageWritableValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { - var diags diag.Diagnostics - - objVal, diags := types.ObjectValue( - map[string]attr.Type{ - "disk_group_id": ovhtypes.TfInt64Type{}, - "hardware_raid": ovhtypes.NewTfListNestedType[StorageHardwareRaidValue](ctx), - "partitioning": StoragePartitioningType{ - basetypes.ObjectType{ - AttrTypes: StoragePartitioningValue{}.AttributeTypes(ctx), - }, - }, - }, - map[string]attr.Value{ - "disk_group_id": v.DiskGroupId, - "hardware_raid": v.HardwareRaid, - "partitioning": v.Partitioning, - }) - - return objVal, diags -} - -func (v StorageWritableValue) Equal(o attr.Value) bool { - other, ok := o.(StorageWritableValue) - - if !ok { - return false - } - - if v.state != other.state { - return false - } - - if v.state != attr.ValueStateKnown { - return true - } - - if !v.DiskGroupId.Equal(other.DiskGroupId) { - return false - } - - if !v.HardwareRaid.Equal(other.HardwareRaid) { - return false - } - - if !v.Partitioning.Equal(other.Partitioning) { - return false - } - - return true -} - var _ basetypes.ObjectTypable = StorageHardwareRaidType{} type StorageHardwareRaidType struct { @@ -3451,30 +3010,6 @@ func (v StorageHardwareRaidValue) ToCreate() *StorageHardwareRaidValue { return res } -func (v StorageHardwareRaidValue) ToUpdate() *StorageHardwareRaidValue { - res := &StorageHardwareRaidValue{} - - if !v.Arrays.IsNull() { - res.Arrays = v.Arrays - } - - if !v.Disks.IsNull() { - res.Disks = v.Disks - } - - if !v.RaidLevel.IsNull() { - res.RaidLevel = v.RaidLevel - } - - if !v.Spares.IsNull() { - res.Spares = v.Spares - } - - res.state = attr.ValueStateKnown - - return res -} - func (v StorageHardwareRaidValue) MarshalJSON() ([]byte, error) { toMarshal := map[string]any{} if !v.Arrays.IsNull() && !v.Arrays.IsUnknown() { @@ -4001,26 +3536,6 @@ func (v StoragePartitioningValue) ToCreate() *StoragePartitioningValue { return res } -func (v StoragePartitioningValue) ToUpdate() *StoragePartitioningValue { - res := &StoragePartitioningValue{} - - if !v.Disks.IsNull() { - res.Disks = v.Disks - } - - if !v.Layout.IsNull() { - res.Layout = v.Layout - } - - if !v.SchemeName.IsNull() { - res.SchemeName = v.SchemeName - } - - res.state = attr.ValueStateKnown - - return res -} - func (v StoragePartitioningValue) MarshalJSON() ([]byte, error) { toMarshal := map[string]any{} if !v.Disks.IsNull() && !v.Disks.IsUnknown() { @@ -4629,34 +4144,6 @@ func (v StoragePartitioningLayoutValue) ToCreate() *StoragePartitioningLayoutVal return res } -func (v StoragePartitioningLayoutValue) ToUpdate() *StoragePartitioningLayoutValue { - res := &StoragePartitioningLayoutValue{} - - if !v.FileSystem.IsNull() { - res.FileSystem = v.FileSystem - } - - if !v.MountPoint.IsNull() { - res.MountPoint = v.MountPoint - } - - if !v.RaidLevel.IsNull() { - res.RaidLevel = v.RaidLevel - } - - if !v.Size.IsNull() { - res.Size = v.Size - } - - if !v.Extras.IsNull() { - res.Extras = v.Extras - } - - res.state = attr.ValueStateKnown - - return res -} - func (v StoragePartitioningLayoutValue) MarshalJSON() ([]byte, error) { toMarshal := map[string]any{} if !v.Extras.IsNull() && !v.Extras.IsUnknown() { @@ -5173,22 +4660,6 @@ func (v StoragePartitioningLayoutExtrasValue) ToCreate() *StoragePartitioningLay return res } -func (v StoragePartitioningLayoutExtrasValue) ToUpdate() *StoragePartitioningLayoutExtrasValue { - res := &StoragePartitioningLayoutExtrasValue{} - - if !v.Zp.IsNull() { - res.Zp = v.Zp - } - - if !v.Lv.IsNull() { - res.Lv = v.Lv - } - - res.state = attr.ValueStateKnown - - return res -} - func (v StoragePartitioningLayoutExtrasValue) MarshalJSON() ([]byte, error) { toMarshal := map[string]any{} if !v.Lv.IsNull() && !v.Lv.IsUnknown() { @@ -5595,18 +5066,6 @@ func (v StoragePartitioningLayoutExtrasLvValue) ToCreate() *StoragePartitioningL return res } -func (v StoragePartitioningLayoutExtrasLvValue) ToUpdate() *StoragePartitioningLayoutExtrasLvValue { - res := &StoragePartitioningLayoutExtrasLvValue{} - - if !v.Name.IsNull() { - res.Name = v.Name - } - - res.state = attr.ValueStateKnown - - return res -} - func (v StoragePartitioningLayoutExtrasLvValue) MarshalJSON() ([]byte, error) { toMarshal := map[string]any{} if !v.Name.IsNull() && !v.Name.IsUnknown() { @@ -5972,18 +5431,6 @@ func (v StoragePartitioningLayoutExtrasZpValue) ToCreate() *StoragePartitioningL return res } -func (v StoragePartitioningLayoutExtrasZpValue) ToUpdate() *StoragePartitioningLayoutExtrasZpValue { - res := &StoragePartitioningLayoutExtrasZpValue{} - - if !v.Name.IsNull() { - res.Name = v.Name - } - - res.state = attr.ValueStateKnown - - return res -} - func (v StoragePartitioningLayoutExtrasZpValue) MarshalJSON() ([]byte, error) { toMarshal := map[string]any{} if !v.Name.IsNull() && !v.Name.IsUnknown() { diff --git a/templates/guides/dedicated_server_migration.md b/templates/guides/dedicated_server_migration.md new file mode 100644 index 000000000..9553834a3 --- /dev/null +++ b/templates/guides/dedicated_server_migration.md @@ -0,0 +1,105 @@ +--- +page_title: "Migrating dedicated servers from previous versions to v2.2.0" +--- + +Version v2.0.0 of OVHcloud Terraform provider introduced a breaking change on the resources related to dedicated servers, mainly: +- Deletion of resource `ovh_dedicated_server_install_task` that has been replaced by a new resource `ovh_dedicated_server_reinstall_task` +- The parameters of resource `ovh_dedicated_server` have been updated to reflect the changes on parameters needed to reinstall a dedicated server + +The complete changelog can be found [here](https://github.com/ovh/terraform-provider-ovh/releases/tag/v2.0.0). + +This guide explains how to migrate your existing configuration relying on resource `ovh_dedicated_server_install_task` to a new configuration compatible with version v2.2.0 of the provider without triggering a reinstallation of your dedicated servers. + +!> This documentation presents a direct migration path between v1.6.0 and v2.2.0. To make this transition easier, we released a version v1.8.0 of the provider that includes both deprecated resources and the new ones. The migration steps are the same, but this version allows a more gradual shift towards v2.2.0. + +## First step: import your dedicated servers in the state + +From version v2.0.0 and later, the preferred way to manage dedicated servers installation details is through usage of resource `ovh_dedicated_server`. As a result, if you don't already have your dedicated servers declared in your Terraform configuration, you must import them. + +You can use an `import` block like the following: + +```terraform +import { + id = "nsxxxxxxx.ip-xx-xx-xx.eu" + to = ovh_dedicated_server.srv +} + +resource "ovh_dedicated_server" "srv" {} +``` + +-> If you are doing a first migration to v1.8.0, you should add the following parameter `prevent_install_on_import = true` to the dedicated server resource. This guarantees you that the server won't be reinstalled after import, even if you have a diff on the reinstall-related parameters. + +To finish importing the resource into your Terraform state, you should run: + +```sh +terraform apply +``` + +## Second step: backport your previous task details into the imported resource + +This step is manual and requires you to convert the previous installation details from resource `ovh_dedicated_server_install_task` to the new fields of resource `ovh_dedicated_server`: `os`, `customizations`, `properties` and `storage`. + +Let's take an example: if you previously used the following configuration: + +```terraform +resource "ovh_dedicated_server_install_task" "server_install" { + service_name = "nsxxxxxxx.ip-xx-xx-xx.eu" + template_name = "debian12_64" + details { + custom_hostname = "mytest" + } + user_metadata { + key = "sshKey" + value = "ssh-ed25519 AAAAC3..." + } + user_metadata { + key = "postInstallationScript" + value = <<-EOF + #!/bin/bash + echo "coucou postInstallationScript" > /opt/coucou + cat /etc/machine-id >> /opt/coucou + date "+%Y-%m-%d %H:%M:%S" --utc >> /opt/coucou + EOF + } +} +``` + +You can replace it by the following one: + +```terraform +resource "ovh_dedicated_server" "srv" { + customizations = { + hostname = "mytest" + post_installation_script = "IyEvYmluL2Jhc2gKZWNobyAiY291Y291IHBvc3RJbnN0YWxsYXRpb25TY3JpcHQiID4gL29wdC9jb3Vjb3UKY2F0IC9ldGMvbWFjaGluZS1pZCAgPj4gL29wdC9jb3Vjb3UKZGF0ZSAiKyVZLSVtLSVkICVIOiVNOiVTIiAtLXV0YyA+PiAvb3B0L2NvdWNvdQo=" + ssh_key = "ssh-ed25519 AAAAC3..." + } + os = "debian12_64" + properties = null + storage = null +} +``` + +You can check the documentation of resource `ovh_dedicated_server` to see what inputs are available for the reinstallation-related fields. +The documentation of resource `ovh_dedicated_server_reinstall_task` also includes several examples of configuration. + +## Third step: make sure your server is not reinstalled unintentionally + +You should add the following piece of configuration in the declaration of your dedicated server resource in order to avoid a reinstallation on the next `terraform apply`: + +```terraform +resource "ovh_dedicated_server" "srv" { + # + # ... resource fields + # + + lifecycle { + ignore_changes = [os, customizations, properties, storage] + } +} +``` + +This is needed because there is no API endpoint that returns the previous installation parameters of a dedicated server. The goal here is to migrate your old configuration to the new format without triggering a reinstallation. + +## Fourth step: remove the lifecycle block + +After a while, whenever you need to trigger a reinstallation of your dedicated server, you can just remove the `lifecycle` field from your configuration and run `terraform apply`. \ No newline at end of file diff --git a/templates/resources/dedicated_server.md.tmpl b/templates/resources/dedicated_server.md.tmpl index f3404639c..6103181a5 100644 --- a/templates/resources/dedicated_server.md.tmpl +++ b/templates/resources/dedicated_server.md.tmpl @@ -37,6 +37,7 @@ Use this resource to order and manage a dedicated server. * `configuration` - Representation of a configuration item to personalize product * `label` - (Required) Identifier of the resource * `value` - (Required) Path to the resource in API.OVH.COM +* `service_name` - (Optional) The service_name of your dedicated server. This field can be used to avoid ordering a dedicated server at creation and just create the resource using an already existing service ### Editable fields of a dedicated server @@ -50,26 +51,27 @@ Use this resource to order and manage a dedicated server. * `state` - All states a Dedicated can in (error, hacked, hackedBlocked, ok) ### Arguments used to reinstall a dedicated server + * `os` - Operating System to install * `customizations` - Customization of the OS configuration - * `configDriveUserData` -Config Drive UserData - * `efiBootloaderPath` - Path of the EFI bootloader from the OS installed on the server + * `config_drive_user_data` - Config Drive UserData + * `efi_bootloader_path` - Path of the EFI bootloader from the OS installed on the server * `hostname` - Custom hostname - * `httpHeaders` - Image HTTP Headers - * `imageCheckSum` - Image checksum - * `imageCheckSumType` - Checksum type - * `imageType` - Image Type - * `imageURL` - Image URL + * `http_headers` - Image HTTP Headers + * `image_check_sum` - Image checksum + * `image_check_sum_type` - Checksum type + * `image_type` - Image Type + * `image_url` - Image URL * `language` - Display Language - * `postInstallationScript` - Post-Installation Script - * `postInstallationScriptExtension` - Post-Installation Script File Extension - * `sshKey` - SSH Public Key + * `post_installation_script` - Post-Installation Script + * `post_installation_script_extension` - Post-Installation Script File Extension + * `ssh_key` - SSH Public Key * `storage` - Storage customization - * `diskGroupId` - Disk group id - * `hardwareRaid` - Hardware Raid configurations + * `disk_group_id` - Disk group id + * `hardware_raid` - Hardware Raid configurations * `arrays` - Number of arrays * `disks` - Total number of disks in the disk group involved in the hardware raid configuration - * `raidLevel` - Hardware raid type + * `raid_level` - Hardware raid type * `spares` - Number of disks in the disk group involved in the spare * `partitioning` - Partitioning configuration * `disks` - Total number of disks in the disk group involved in the partitioning configuration @@ -77,13 +79,19 @@ Use this resource to order and manage a dedicated server. * `extras` - Partition extras parameters * `lv` - LVM-specific parameters * `zp` - ZFS-specific parameters - * `fileSystem` - File system type - * `mountPoint` - Mount point - * `raidLevel` - Software raid type + * `file_system` - File system type + * `mount_point` - Mount point + * `raid_level` - Software raid type * `size` - Partition size in MiB - * `schemeName` - Partitioning scheme (if applicable with selected operating system) + * `scheme_name` - Partitioning scheme (if applicable with selected operating system) * `properties` - Arbitrary properties to pass to cloud-init's config drive datasource +### Arguments used to control the lifecycle of a dedicated server + +* `keep_service_after_destroy` - Avoid termination of the service when deleting the resource (when using this parameter, make sure to apply your configuration before running the destroy so that the value is set in the state) +* `prevent_install_on_create` - Prevent server installation after it has been delivered +* `prevent_install_on_import` - Defines whether a reinstallation of the server is allowed after importing it if there is a modification on the installation parameters + ## Attributes Reference * `service_name` - The service_name of your dedicated server diff --git a/templates/resources/dedicated_server_reinstall_task.md.tmpl b/templates/resources/dedicated_server_reinstall_task.md.tmpl index af003ba79..7019362bc 100644 --- a/templates/resources/dedicated_server_reinstall_task.md.tmpl +++ b/templates/resources/dedicated_server_reinstall_task.md.tmpl @@ -126,11 +126,3 @@ The following attributes are exported: * `function` - Function name (should be `hardInstall`). * `start_date` - Task creation date in RFC3339 format. * `status` - Task status (should be `done`) - -## Import - -Installation task can be imported using the `service_name` (`nsXXXX.ip...`) of the baremetal server, the `operating_system` used and ths `task_id`, separated by "/" E.g., - -```bash -terraform import ovh_dedicated_server_reinstall_task nsXXXX.ipXXXX/operating_system/12345 -```