Repository: ovh/terraform-provider-ovh

Issue:
[BUG] 'Unknown parameter' when creating an instance with an existing floating_ip
### Describe the bug
When trying to create an instance with an existing floating_ip attached terraform throws an error:
```
│ Error: calling /cloud/project/[PROJECT_ID]/region/DE1/instance with params &{<nil> <nil> monthly 0xc0007a4e40 0 0xc0007a4c30 <nil> [INSTANCE_NAME] 0xc0007a5060 <nil> <nil> 0xc0007a5b70}:
│ 	"OVHcloud API error (status code 400): Client::BadRequest: \"[network.private.floating_ip] Unknown parameter\" (X-OVH-Query-Id: EU.ext-5.6822f679.2410674.a09c6bb6944492cd7981f4ddce2997df)"
│ 
│   with ovh_cloud_project_instance.node_01,
│   on vpn_ovh.tf line 78, in resource "ovh_cloud_project_instance" "node_01":
│   78: resource "ovh_cloud_project_instance" "node_01" {
```

### Terraform Version

Terraform v1.11.3

### OVH Terraform Provider Version

2.2.0

### Affected Resource(s)

- ovh_cloud_project_instance

### Terraform Configuration Files

```hcl
resource "ovh_cloud_project_instance" "node_01" {
  service_name   = ovh_cloud_project.vpn_dc_ovh.id
  region         = "DE1"
  billing_period = "monthly"
  boot_from {
    image_id = "1241ac38-c075-44bf-a91e-230e72f078b9"
  }
  flavor {
    flavor_id = "6c693dba-c8e8-4ead-a095-46425b43dc0f"
  }
  name = "INSTANCE_NAME"
  ssh_key {
    name = "KEY_NAME"
  }
  network {
    public = false
    private {
      network {
        id        = tolist(ovh_cloud_project_network_private.net.regions_attributes[*].openstackid)[0]
        subnet_id = ovh_cloud_project_network_private_subnet.subnet.id
      }
      gateway {
        id = ovh_cloud_project_gateway.gateway.id
      }
      floating_ip {
        # https://eu.api.ovh.com/console/?section=%2Fcloud&branch=v1#get-/cloud/project/-serviceName-/region/-regionName-/floatingip
        id = "87d52c02-66f2-4d3e-80c1-30b7c4ebf562"
      }
    }
  }
}

```

### Debug Output

### Panic Output

### Expected Behavior

Instance gehts created with attached floating_ip

### Actual Behavior

Terraform throws error

### Steps to Reproduce

Please list the steps required to reproduce the issue, for example:
1. `terraform apply`

### References

### Additional context


Hints from issue comments:
@amstuta: Hello @MartiniMoe, thanks for opening this issue.

Indeed the parameter used for the floating IP when calling the API is not the right one, we will fix that shortly!

Solution patch:
diff --git a/docs/resources/cloud_project_database_log_subscription.md b/docs/resources/cloud_project_database_log_subscription.md
index 1a2311ee..b7933e2c 100644
--- a/docs/resources/cloud_project_database_log_subscription.md
+++ b/docs/resources/cloud_project_database_log_subscription.md
@@ -17,9 +17,9 @@ data "ovh_dbaas_logs_output_graylog_stream" "stream" {
 }
 
 data "ovh_cloud_project_database" "db" {
-  service_name  = "XXX"
-  engine        = "YYY"
-  id            = "ZZZ"
+  service_name = "XXX"
+  engine       = "YYY"
+  id           = "ZZZ"
 }
 
 resource "ovh_cloud_project_database_log_subscription" "subscription" {
diff --git a/docs/resources/dedicated_server.md b/docs/resources/dedicated_server.md
index f015d63a..5a7ca7b0 100644
--- a/docs/resources/dedicated_server.md
+++ b/docs/resources/dedicated_server.md
@@ -142,7 +142,9 @@ resource "ovh_dedicated_server" "server" {
 ### 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)
-* `run_actions_before_destroy` - (Set of strings) Actions to perform before destroying the server. Right now the only available action is "reinstall_only_os", that will reinstall the dedicated server with the operating system defined in the `os` field. When using this parameter, make sure to apply your configuration before running the destroy so that the value is set in the state
+* `run_actions_before_destroy` - (Set of strings) Actions to perform before destroying the server. When using this parameter, make sure to apply your configuration before running the destroy so that the value is set in the state. The following actions are available:
+  * "reinstall_only_os": Will reinstall the dedicated server with the operating system defined in the `os` field.
+  * "reboot_rescue": Will reboot your server in rescue mode before destroying the resource.
 * `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
 
diff --git a/ovh/resource_dedicated_server.go b/ovh/resource_dedicated_server.go
index 69dc6c6d..cad88988 100644
--- a/ovh/resource_dedicated_server.go
+++ b/ovh/resource_dedicated_server.go
@@ -382,6 +382,10 @@ func (r *dedicatedServerResource) runPreDestroyActions(ctx context.Context, data
 			if err := r.reinstallDedicatedServer(ctx, false, true, nil, data); err != nil {
 				return fmt.Errorf("failed to reinstall server: %w", err)
 			}
+		case "reboot_rescue":
+			if err := r.rebootRescue(ctx, data); err != nil {
+				return fmt.Errorf("failed to reboot server in rescue mode: %w", err)
+			}
 		default:
 			return fmt.Errorf("invalid pre-destroy action given: %q", action)
 		}
@@ -390,6 +394,42 @@ func (r *dedicatedServerResource) runPreDestroyActions(ctx context.Context, data
 	return nil
 }
 
+func (r *dedicatedServerResource) rebootRescue(ctx context.Context, data *DedicatedServerModel) error {
+	// List available boots
+	var boots []int
+	endpoint := fmt.Sprintf("/dedicated/server/%s/boot?bootType=rescue", data.ServiceName.ValueString())
+	if err := r.config.OVHClient.GetWithContext(ctx, endpoint, &boots); err != nil {
+		return fmt.Errorf("failed to fetch boot options: %w", err)
+	}
+
+	if len(boots) == 0 {
+		return errors.New("no boot found for rescue mode")
+	}
+
+	// Update server with boot ID
+	endpoint = fmt.Sprintf("/dedicated/server/%s", data.ServiceName.ValueString())
+	if err := r.config.OVHClient.PutWithContext(ctx, endpoint, map[string]any{
+		"bootId": boots[0],
+	}, nil); err != nil {
+		return fmt.Errorf("failed to set boot ID %d for server: %w", boots[0], err)
+	}
+
+	// Reboot server
+	endpoint += "/reboot"
+	task := &DedicatedServerTask{}
+
+	if err := r.config.OVHClient.Post(endpoint, nil, task); err != nil {
+		return fmt.Errorf("failed to reboot server: %w", err)
+	}
+
+	// Wait for reboot to be done
+	if err := waitForDedicatedServerTask(data.ServiceName.ValueString(), task, r.config.OVHClient); err != nil {
+		return fmt.Errorf("error waiting for reboot task: %w", err)
+	}
+
+	return nil
+}
+
 func (r *dedicatedServerResource) reinstallDedicatedServer(ctx context.Context, preventReinstall, onlyOS 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))
diff --git a/ovh/resource_dedicated_server_gen.go b/ovh/resource_dedicated_server_gen.go
index b3cb41df..9ef08055 100644
--- a/ovh/resource_dedicated_server_gen.go
+++ b/ovh/resource_dedicated_server_gen.go
@@ -262,7 +262,7 @@ func DedicatedServerResourceSchema(ctx context.Context) schema.Schema {
 			Optional:    true,
 			Description: "Actions to run before destroying the resource",
 			Validators: []validator.Set{
-				setvalidator.ValueStringsAre(stringvalidator.OneOf("reinstall_only_os")),
+				setvalidator.ValueStringsAre(stringvalidator.OneOf("reinstall_only_os", "reboot_rescue")),
 			},
 		},
 		"link_speed": schema.Int64Attribute{
diff --git a/ovh/types_cloud_project_instance.go b/ovh/types_cloud_project_instance.go
index af9fc190..28490dcf 100644
--- a/ovh/types_cloud_project_instance.go
+++ b/ovh/types_cloud_project_instance.go
@@ -45,7 +45,7 @@ type Network struct {
 }
 
 type PrivateNetwork struct {
-	FloatingIP       *PrivateNetworkFloatingIP        `json:"floating_ip,omitempty"`
+	FloatingIP       *PrivateNetworkFloatingIP        `json:"floatingIp,omitempty"`
 	FloatingIPCreate *PrivateNetworkFloatingIPCreate  `json:"floatingIpCreate,omitempty"`
 	Gateway          *PrivateNetworkGateway           `json:"gateway,omitempty"`
 	GatewayCreate    *PrivateNetworkGatewayCreate     `json:"gatewayCreate,omitempty"`
diff --git a/templates/resources/dedicated_server.md.tmpl b/templates/resources/dedicated_server.md.tmpl
index c3c7abd9..25b21b8b 100644
--- a/templates/resources/dedicated_server.md.tmpl
+++ b/templates/resources/dedicated_server.md.tmpl
@@ -89,7 +89,9 @@ Use this resource to order and manage a dedicated server.
 ### 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)
-* `run_actions_before_destroy` - (Set of strings) Actions to perform before destroying the server. Right now the only available action is "reinstall_only_os", that will reinstall the dedicated server with the operating system defined in the `os` field. When using this parameter, make sure to apply your configuration before running the destroy so that the value is set in the state
+* `run_actions_before_destroy` - (Set of strings) Actions to perform before destroying the server. When using this parameter, make sure to apply your configuration before running the destroy so that the value is set in the state. The following actions are available:
+  * "reinstall_only_os": Will reinstall the dedicated server with the operating system defined in the `os` field.
+  * "reboot_rescue": Will reboot your server in rescue mode before destroying the resource.
 * `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
 



Test patch:
diff --git a/ovh/provider_test.go b/ovh/provider_test.go
index 3d4e4591..88fe1cc1 100644
--- a/ovh/provider_test.go
+++ b/ovh/provider_test.go
@@ -481,6 +481,14 @@ func testAccPreCheckOkmsCredential(t *testing.T) {
 	checkEnvOrSkip(t, "OVH_OKMS_CREDENTIAL")
 }
 
+func testAccPreCheckCloudInstance(t *testing.T) {
+	testAccPreCheckCredentials(t)
+	checkEnvOrSkip(t, "OVH_CLOUD_PROJECT_NETWORK_PRIVATE_TEST")
+	checkEnvOrSkip(t, "OVH_CLOUD_PROJECT_NETWORK_PRIVATE_SUBNET_TEST")
+	checkEnvOrSkip(t, "OVH_CLOUD_PROJECT_FLOATING_IP_ID")
+	checkEnvOrSkip(t, "OVH_CLOUD_PROJECT_GATEWAY_ID")
+}
+
 func testAccCheckVRackExists(t *testing.T) {
 	type vrackResponse struct {
 		Name        string `json:"name"`
diff --git a/ovh/resource_cloud_project_instance_test.go b/ovh/resource_cloud_project_instance_test.go
index 8c8e49ed..27429c7a 100644
--- a/ovh/resource_cloud_project_instance_test.go
+++ b/ovh/resource_cloud_project_instance_test.go
@@ -230,7 +230,7 @@ func TestAccCloudProjectInstance_withSSHKeyCreate(t *testing.T) {
 	})
 }
 
-func TestAccCloudProjectInstance_privateNetwork(t *testing.T) {
+func TestAccCloudProjectInstance_privateNetworkCreate(t *testing.T) {
 	serviceName := os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST")
 	region := os.Getenv("OVH_CLOUD_PROJECT_REGION_TEST")
 	flavor, image, err := getFlavorAndImage(serviceName, region)
@@ -299,3 +299,78 @@ func TestAccCloudProjectInstance_privateNetwork(t *testing.T) {
 		},
 	})
 }
+
+func TestAccCloudProjectInstance_privateNetworkAlreadyExists(t *testing.T) {
+	serviceName := os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST")
+	region := os.Getenv("OVH_CLOUD_PROJECT_REGION_TEST")
+	flavor, image, err := getFlavorAndImage(serviceName, region)
+	if err != nil {
+		t.Skipf("failed to retrieve a flavor and an image: %s", err)
+	}
+
+	networkID := os.Getenv("OVH_CLOUD_PROJECT_NETWORK_PRIVATE_TEST")
+	subnetID := os.Getenv("OVH_CLOUD_PROJECT_NETWORK_PRIVATE_SUBNET_TEST")
+	floatingIpID := os.Getenv("OVH_CLOUD_PROJECT_FLOATING_IP_ID")
+	gatewayID := os.Getenv("OVH_CLOUD_PROJECT_GATEWAY_ID")
+
+	var testCreateInstance = fmt.Sprintf(`
+			resource "ovh_cloud_project_instance" "instance" {
+				service_name = "%s"
+				region = "%s"
+				billing_period = "hourly"
+				boot_from {
+					image_id = "%s"
+				}
+				flavor {
+					flavor_id = "%s"
+				}
+				name = "TestInstance"
+				ssh_key {
+					name = "%s"
+				}
+				network {
+					private {
+						floating_ip {
+							id = "%s"
+						}
+						network {
+							id = "%s"
+							subnet_id = "%s"
+						}
+						gateway {
+							id = "%s"
+						}
+					}
+				}
+			}
+		`,
+		serviceName,
+		region,
+		image,
+		flavor,
+		os.Getenv("OVH_CLOUD_PROJECT_SSH_NAME_TEST"),
+		floatingIpID, networkID, subnetID, gatewayID)
+
+	resource.Test(t, resource.TestCase{
+		PreCheck: func() {
+			testAccPreCheckCloud(t)
+			testAccCheckCloudProjectExists(t)
+			testAccPreCheckCloudInstance(t)
+		},
+		Providers: testAccProviders,
+		Steps: []resource.TestStep{
+			{
+				Config: testCreateInstance,
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttrSet("ovh_cloud_project_instance.instance", "id"),
+					resource.TestCheckResourceAttr("ovh_cloud_project_instance.instance", "flavor_name", "b2-7"),
+					resource.TestCheckResourceAttr("ovh_cloud_project_instance.instance", "flavor_id", flavor),
+					resource.TestCheckResourceAttr("ovh_cloud_project_instance.instance", "image_id", image),
+					resource.TestCheckResourceAttr("ovh_cloud_project_instance.instance", "region", region),
+					resource.TestCheckResourceAttr("ovh_cloud_project_instance.instance", "name", "TestInstance"),
+					resource.TestCheckResourceAttrSet("ovh_cloud_project_instance.instance", "addresses.0.ip"),
+				),
+			},
+		},
+	})
+}