diff --git a/.cds/terraform-provider-ovh.yml b/.cds/terraform-provider-ovh.yml index 17c53ba4..08087773 100644 --- a/.cds/terraform-provider-ovh.yml +++ b/.cds/terraform-provider-ovh.yml @@ -568,6 +568,18 @@ workflow: pipeline: terraform-provider-ovh-testacc when: - success + Tests_TestAccCloudProjectInstance: + application: terraform-provider-ovh + depends_on: + - terraform-provider-ovh-pre-sweepers + environment: acctests + one_at_a_time: true + parameters: + testargs: -run CloudProjectInstance + skipthispipeline: "{{.workflow.terraform-provider-ovh.pip.skipthistest.cloudproject}}" + pipeline: terraform-provider-ovh-testacc + when: + - success Tests_TestAccCloudProjectWorkflowBackup: application: terraform-provider-ovh depends_on: @@ -807,6 +819,7 @@ workflow: - Tests_CloudProjectUser - Tests_CloudProjectLoadBalancer - Tests_CloudProjectVolume + - Tests_TestAccCloudProjectInstance - Tests_DbaasLogs - Tests_DedicatedCeph - Tests_TestAccDedicatedInstallationTemplate diff --git a/ovh/data_cloud_project_instance.go b/ovh/data_cloud_project_instance.go new file mode 100644 index 00000000..103fa15c --- /dev/null +++ b/ovh/data_cloud_project_instance.go @@ -0,0 +1,145 @@ +package ovh + +import ( + "fmt" + "log" + "net/url" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/ovh/terraform-provider-ovh/ovh/helpers" +) + +func dataSourceCloudProjectInstance() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudProjectInstanceRead, + Schema: map[string]*schema.Schema{ + "service_name": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("OVH_CLOUD_PROJECT_SERVICE", nil), + Description: "Service name of the resource representing the id of the cloud project", + }, + "region": { + Type: schema.TypeString, + Description: "Instance region", + Required: true, + }, + "instance_id": { + Type: schema.TypeString, + Description: "Instance id", + Required: true, + }, + // computed + "addresses": { + Type: schema.TypeSet, + Computed: true, + Description: "Instance IP addresses", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Description: "IP address", + Computed: true, + }, + "version": { + Type: schema.TypeInt, + Description: "IP version", + Computed: true, + }, + }, + }, + }, + "attached_volumes": { + Type: schema.TypeSet, + Computed: true, + Description: "Volumes attached to the instance", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Volume id", + Computed: true, + }, + }, + }, + }, + "flavor_id": { + Type: schema.TypeString, + Description: "Flavor id", + Computed: true, + }, + "flavor_name": { + Type: schema.TypeString, + Description: "Flavor name", + Computed: true, + }, + "name": { + Type: schema.TypeString, + Description: "Instance name", + Computed: true, + }, + "image_id": { + Type: schema.TypeString, + Description: "Image id", + Computed: true, + }, + "ssh_key": { + Type: schema.TypeString, + Description: "SSH Key pair name", + Computed: true, + }, + "task_state": { + Type: schema.TypeString, + Description: "Instance task state", + Computed: true, + }, + }, + } +} + +func dataSourceCloudProjectInstanceRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + serviceName := d.Get("service_name").(string) + region := d.Get("region").(string) + instanceId := d.Get("instance_id").(string) + + endpoint := fmt.Sprintf("/cloud/project/%s/region/%s/instance/%s", + url.PathEscape(serviceName), + url.PathEscape(region), + url.PathEscape(instanceId), + ) + var res CloudProjectInstanceResponse + + log.Printf("[DEBUG] Will read instance %s from project %s in region %s", instanceId, serviceName, region) + if err := config.OVHClient.Get(endpoint, &res); err != nil { + return helpers.CheckDeleted(d, err, endpoint) + } + + addresses := make([]map[string]interface{}, 0, len(res.Addresses)) + for _, addr := range res.Addresses { + address := make(map[string]interface{}) + address["ip"] = addr.Ip + address["version"] = addr.Version + addresses = append(addresses, address) + } + + attachedVolumes := make([]map[string]interface{}, 0, len(res.AttachedVolumes)) + for _, volume := range res.AttachedVolumes { + attachedVolume := make(map[string]interface{}) + attachedVolume["id"] = volume.Id + attachedVolumes = append(attachedVolumes, attachedVolume) + } + + d.Set("addresses", addresses) + d.Set("flavor_id", res.FlavorId) + d.Set("flavor_name", res.FlavorName) + d.SetId(res.Id) + d.Set("image_id", res.ImageId) + d.Set("instance_id", res.Id) + d.Set("name", res.Name) + d.Set("ssh_key", res.SshKey) + d.Set("task_state", res.TaskState) + d.Set("attached_volumes", attachedVolumes) + + return nil +} diff --git a/ovh/data_cloud_project_instance_test.go b/ovh/data_cloud_project_instance_test.go new file mode 100644 index 00000000..82410a9b --- /dev/null +++ b/ovh/data_cloud_project_instance_test.go @@ -0,0 +1,46 @@ +package ovh + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDataSourceCloudProjectInstance_basic(t *testing.T) { + config := fmt.Sprintf(` + data "ovh_cloud_project_instance" "test" { + service_name = "%s" + region = "%s" + instance_id = "%s" + } + `, + os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST"), + os.Getenv("OVH_CLOUD_PROJECT_REGION_TEST"), + os.Getenv("OVH_CLOUD_PROJECT_INSTANCE_ID_TEST"), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckCloud(t) + testAccCheckCloudProjectExists(t) + }, + + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.ovh_cloud_project_instance.test", "flavor_name"), + resource.TestCheckResourceAttrSet("data.ovh_cloud_project_instance.test", "flavor_id"), + resource.TestCheckResourceAttrSet("data.ovh_cloud_project_instance.test", "id"), + resource.TestCheckResourceAttrSet("data.ovh_cloud_project_instance.test", "image_id"), + resource.TestCheckResourceAttrSet("data.ovh_cloud_project_instance.test", "name"), + resource.TestCheckResourceAttrSet("data.ovh_cloud_project_instance.test", "ssh_key"), + resource.TestCheckResourceAttrSet("data.ovh_cloud_project_instance.test", "region"), + ), + }, + }, + }) +} diff --git a/ovh/data_cloud_project_instances.go b/ovh/data_cloud_project_instances.go new file mode 100644 index 00000000..9abc9554 --- /dev/null +++ b/ovh/data_cloud_project_instances.go @@ -0,0 +1,140 @@ +package ovh + +import ( + "fmt" + "log" + "net/url" + "sort" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/ovh/terraform-provider-ovh/ovh/helpers" + "github.com/ovh/terraform-provider-ovh/ovh/helpers/hashcode" +) + +func dataSourceCloudProjectInstances() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudProjectInstancesRead, + Schema: map[string]*schema.Schema{ + "service_name": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("OVH_CLOUD_PROJECT_SERVICE", nil), + Description: "Service name of the resource representing the id of the cloud project", + }, + "region": { + Type: schema.TypeString, + Description: "Instance region", + Required: true, + }, + // computed + "instances": { + Type: schema.TypeSet, + Description: "List of instances", + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "addresses": { + Type: schema.TypeSet, + Computed: true, + Description: "Instance IP addresses", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Description: "IP address", + Computed: true, + }, + "version": { + Type: schema.TypeInt, + Description: "IP version", + Computed: true, + }, + }, + }, + }, + "attached_volumes": { + Type: schema.TypeList, + Computed: true, + Description: "Volumes attached to the instance", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Volume id", + Computed: true, + }, + }, + }, + }, + "flavor_id": { + Type: schema.TypeString, + Description: "Flavor id", + Computed: true, + }, + "flavor_name": { + Type: schema.TypeString, + Description: "Flavor name", + Computed: true, + }, + "name": { + Type: schema.TypeString, + Description: "Instance name", + Computed: true, + }, + "id": { + Type: schema.TypeString, + Description: "Instance id", + Computed: true, + }, + "image_id": { + Type: schema.TypeString, + Description: "Image id", + Computed: true, + }, + "ssh_key": { + Type: schema.TypeString, + Description: "SSH Key pair name", + Computed: true, + }, + "task_state": { + Type: schema.TypeString, + Description: "Instance task state", + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceCloudProjectInstancesRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + serviceName := d.Get("service_name").(string) + region := d.Get("region").(string) + + endpoint := fmt.Sprintf("/cloud/project/%s/region/%s/instance", + url.PathEscape(serviceName), + url.PathEscape(region), + ) + var res []CloudProjectInstanceResponse + + log.Printf("[DEBUG] Will read instances from project %s in region %s", serviceName, region) + if err := config.OVHClient.Get(endpoint, &res); err != nil { + return helpers.CheckDeleted(d, err, endpoint) + } + + instances := make([]map[string]interface{}, 0, len(res)) + ids := make([]string, 0, len(res)) + for _, instance := range res { + instances = append(instances, instance.ToMap()) + ids = append(ids, instance.Id) + } + sort.Strings(ids) + + d.SetId(hashcode.Strings(ids)) + d.Set("instances", instances) + + log.Printf("[DEBUG] Read instances: %s", ids) + return nil +} diff --git a/ovh/data_cloud_project_instances_test.go b/ovh/data_cloud_project_instances_test.go new file mode 100644 index 00000000..084ebd63 --- /dev/null +++ b/ovh/data_cloud_project_instances_test.go @@ -0,0 +1,57 @@ +package ovh + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDataSourceCloudProjectInstances_basic(t *testing.T) { + config := fmt.Sprintf(` + data "ovh_cloud_project_instances" "instances" { + service_name = "%s" + region = "%s" + } + `, + os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST"), + os.Getenv("OVH_CLOUD_PROJECT_REGION_TEST"), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckCloud(t) + testAccCheckCloudProjectExists(t) + }, + + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "data.ovh_cloud_project_instances.instances", + "instances.0.flavor_id", + ), + resource.TestCheckResourceAttrSet( + "data.ovh_cloud_project_instances.instances", + "instances.0.flavor_name", + ), + resource.TestCheckResourceAttrSet( + "data.ovh_cloud_project_instances.instances", + "instances.0.id", + ), + resource.TestCheckResourceAttrSet( + "data.ovh_cloud_project_instances.instances", + "instances.0.image_id", + ), + resource.TestCheckResourceAttrSet( + "data.ovh_cloud_project_instances.instances", + "instances.0.ssh_key", + ), + ), + }, + }, + }) +} diff --git a/ovh/provider.go b/ovh/provider.go index 2cf7d9ac..602b391d 100644 --- a/ovh/provider.go +++ b/ovh/provider.go @@ -116,6 +116,8 @@ func Provider() *schema.Provider { "ovh_cloud_project_database_user": dataSourceCloudProjectDatabaseUser(), "ovh_cloud_project_database_users": dataSourceCloudProjectDatabaseUsers(), "ovh_cloud_project_failover_ip_attach": dataSourceCloudProjectFailoverIpAttach(), + "ovh_cloud_project_instance": dataSourceCloudProjectInstance(), + "ovh_cloud_project_instances": dataSourceCloudProjectInstances(), "ovh_cloud_project_kube": dataSourceCloudProjectKube(), "ovh_cloud_project_kube_iprestrictions": dataSourceCloudProjectKubeIPRestrictions(), "ovh_cloud_project_kube_nodepool_nodes": dataSourceCloudProjectKubeNodepoolNodes(), @@ -211,6 +213,7 @@ func Provider() *schema.Provider { "ovh_cloud_project_database_user": resourceCloudProjectDatabaseUser(), "ovh_cloud_project_failover_ip_attach": resourceCloudProjectFailoverIpAttach(), "ovh_cloud_project_gateway": resourceCloudProjectGateway(), + "ovh_cloud_project_instance": resourceCloudProjectInstance(), "ovh_cloud_project_kube": resourceCloudProjectKube(), "ovh_cloud_project_kube_nodepool": resourceCloudProjectKubeNodePool(), "ovh_cloud_project_kube_oidc": resourceCloudProjectKubeOIDC(), diff --git a/ovh/resource_cloud_project_instance.go b/ovh/resource_cloud_project_instance.go new file mode 100644 index 00000000..834a0509 --- /dev/null +++ b/ovh/resource_cloud_project_instance.go @@ -0,0 +1,345 @@ +package ovh + +import ( + "context" + "fmt" + "log" + "net/url" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/ovh/terraform-provider-ovh/ovh/helpers" +) + +func resourceCloudProjectInstance() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceCloudProjectInstanceCreate, + ReadContext: resourceCloudProjectInstanceRead, + DeleteContext: resourceCloudProjectInstanceDelete, + + Schema: map[string]*schema.Schema{ + "service_name": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("OVH_CLOUD_PROJECT_SERVICE", nil), + Description: "Service name of the resource representing the id of the cloud project", + ForceNew: true, + }, + "region": { + Type: schema.TypeString, + Description: "Instance region", + Required: true, + ForceNew: true, + }, + "auto_backup": { + Type: schema.TypeSet, + MaxItems: 1, + Optional: true, + Description: "Create an autobackup workflow after instance start up", + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cron": { + Type: schema.TypeString, + Description: "Unix cron pattern", + ForceNew: true, + Required: true, + }, + "rotation": { + Type: schema.TypeInt, + Description: "Number of backup to keep", + ForceNew: true, + Required: true, + }, + }, + }, + }, + "billing_period": { + Type: schema.TypeString, + Description: "Billing period - hourly | monthly ", + Required: true, + ForceNew: true, + ValidateFunc: helpers.ValidateEnum([]string{"monthly", "hourly"}), + }, + "boot_from": { + Type: schema.TypeSet, + Required: true, + MaxItems: 1, + Description: "Boot the instance from an image or a volume", + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "image_id": { + Type: schema.TypeString, + Description: "Instance image id", + Optional: true, + }, + "volume_id": { + Type: schema.TypeString, + Description: "Instance volume id", + Optional: true, + }, + }, + }, + }, + "bulk": { + Type: schema.TypeInt, + Description: "Create multiple instances", + Optional: true, + ForceNew: true, + }, + "flavor": { + Type: schema.TypeSet, + Required: true, + MaxItems: 1, + Description: "Flavor information", + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "flavor_id": { + Type: schema.TypeString, + Description: "Flavor id", + Required: true, + }, + }, + }, + }, + "group": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Description: "Start instance in group", + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "group_id": { + Type: schema.TypeString, + Description: "Group id", + Optional: true, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Description: "Instance name", + Required: true, + ForceNew: true, + }, + "ssh_key": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + MaxItems: 1, + Description: "Existing SSH Key pair", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "SSH Key pair name", + Required: true, + }, + }, + }, + }, + "ssh_key_create": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + MaxItems: 1, + Description: "Add existing SSH Key pair into your Public Cloud project and link it to the instance", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "SSH Key pair name", + Required: true, + }, + "public_key": { + Type: schema.TypeString, + Description: "SSH Public Key", + Required: true, + }, + }, + }, + }, + "user_data": { + Type: schema.TypeString, + Description: "Configuration information or scripts to use upon launch", + Optional: true, + ForceNew: true, + }, + "network": { + Type: schema.TypeSet, + Required: true, + ForceNew: true, + MaxItems: 1, + Description: "Create network interfaces", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "public": { + Type: schema.TypeBool, + Description: "Set the new instance as public", + Optional: true, + }, + }, + }, + }, + // computed + "addresses": { + Type: schema.TypeSet, + Computed: true, + Description: "Instance IP addresses", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Description: "IP address", + Computed: true, + }, + "version": { + Type: schema.TypeInt, + Description: "IP version", + Computed: true, + }, + }, + }, + }, + "attached_volumes": { + Type: schema.TypeSet, + Computed: true, + Description: "Volumes attached to the instance", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Volume id", + Computed: true, + }, + }, + }, + }, + "flavor_id": { + Type: schema.TypeString, + Description: "Flavor id", + Computed: true, + }, + "flavor_name": { + Type: schema.TypeString, + Description: "Flavor name", + Computed: true, + }, + "id": { + Type: schema.TypeString, + Description: "Instance id", + Computed: true, + }, + "image_id": { + Type: schema.TypeString, + Description: "Image id", + Computed: true, + }, + "task_state": { + Type: schema.TypeString, + Description: "Instance task state", + Computed: true, + }, + }, + } +} + +func resourceCloudProjectInstanceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + serviceName := d.Get("service_name").(string) + region := d.Get("region").(string) + params := new(CloudProjectInstanceCreateOpts) + params.FromResource(d) + + endpoint := fmt.Sprintf("/cloud/project/%s/region/%s/instance", + url.PathEscape(serviceName), + url.PathEscape(region), + ) + + r := &CloudProjectOperationResponse{} + if err := config.OVHClient.Post(endpoint, params, r); err != nil { + return diag.Errorf("calling %s with params %v:\n\t %q", endpoint, params, err) + } + + instanceID, err := waitForCloudProjectOperation(ctx, config.OVHClient, serviceName, r.Id) + if err != nil { + return diag.Errorf("timeout instance creation: %s", err) + } + + d.SetId(instanceID) + d.Set("region", region) + + return resourceCloudProjectInstanceRead(ctx, d, meta) +} + +func resourceCloudProjectInstanceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + id := d.Id() + region := d.Get("region").(string) + serviceName := d.Get("service_name").(string) + + endpoint := fmt.Sprintf("/cloud/project/%s/region/%s/instance/%s", + url.PathEscape(serviceName), + url.PathEscape(region), + url.PathEscape(id), + ) + + r := &CloudProjectInstanceResponse{} + if err := config.OVHClient.Get(endpoint, r); err != nil { + return diag.Errorf("Error calling Get %s:\n\t %q", endpoint, err) + } + + d.Set("flavor_id", r.FlavorId) + d.Set("flavor_name", r.FlavorName) + d.Set("image_id", r.ImageId) + d.Set("region", r.Region) + d.Set("task_state", r.TaskState) + d.Set("id", r.Id) + + addresses := make([]map[string]interface{}, 0, len(r.Addresses)) + for _, add := range r.Addresses { + address := make(map[string]interface{}) + address["ip"] = add.Ip + address["version"] = add.Version + addresses = append(addresses, address) + } + d.Set("addresses", addresses) + + attachedVolumes := make([]map[string]interface{}, 0, len(r.AttachedVolumes)) + for _, att := range r.AttachedVolumes { + attachedVolume := make(map[string]interface{}) + attachedVolume["id"] = att.Id + attachedVolumes = append(attachedVolumes, attachedVolume) + } + d.Set("attached_volumes", attachedVolumes) + + return nil +} + +func resourceCloudProjectInstanceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + serviceName := d.Get("service_name").(string) + region := d.Get("region").(string) + + id := d.Id() + + log.Printf("[DEBUG] Will delete public cloud instance for project: %s, region: %s, id: %s", serviceName, region, id) + + endpoint := fmt.Sprintf("/cloud/project/%s/instance/%s", + url.PathEscape(serviceName), + url.PathEscape(id), + ) + + if err := config.OVHClient.Delete(endpoint, nil); err != nil { + return diag.Errorf("Error calling Delete %s:\n\t %q", endpoint, err) + } + + d.SetId("") + + log.Printf("[DEBUG] Deleted Public Cloud %s Instance %s", serviceName, id) + return nil +} diff --git a/ovh/resource_cloud_project_instance_test.go b/ovh/resource_cloud_project_instance_test.go new file mode 100644 index 00000000..51ae8510 --- /dev/null +++ b/ovh/resource_cloud_project_instance_test.go @@ -0,0 +1,106 @@ +package ovh + +import ( + "fmt" + "net/url" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func getFlavorAndImage(project, region string) (string, string, error) { + client, err := clientDefault(&Config{}) + if err != nil { + return "", "", fmt.Errorf("error getting client: %w", err) + } + + type ResponseStruct struct { + ID string `json:"id"` + Type string `json:"type"` + OSType string `json:"osType"` + } + + endpoint := fmt.Sprintf("/cloud/project/%s/flavor?region=%s", url.PathEscape(project), url.QueryEscape(region)) + + var response []*ResponseStruct + if err := client.Get(endpoint, &response); err != nil { + return "", "", fmt.Errorf("failed to get flavors: %w", err) + } + + for _, flav := range response { + endpoint = fmt.Sprintf("/cloud/project/%s/image?region=%s&osType=%s&flavorType=%s", + url.PathEscape(project), + url.QueryEscape(region), + url.QueryEscape(flav.OSType), + url.QueryEscape(flav.Type), + ) + + var images []*ResponseStruct + if err := client.Get(endpoint, &images); err != nil { + return "", "", fmt.Errorf("failed to get images: %w", err) + } + + if len(images) > 0 { + return flav.ID, images[0].ID, nil + } + } + + return "", "", fmt.Errorf("found no flavor and image for project %s and region %s", project, region) +} + +func TestAccCloudProjectInstance_basic(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.Fatalf("failed to retrieve a flavor and an image: %s", err) + } + + 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 { + public = true + } + } + `, + serviceName, + region, + image, + flavor, + os.Getenv("OVH_CLOUD_PROJECT_SSH_NAME_TEST")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckCloud(t) + testAccCheckCloudProjectExists(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"), + ), + }, + }, + }) +} diff --git a/ovh/types_cloud.go b/ovh/types_cloud.go index 824ddc74..2e29ed77 100644 --- a/ovh/types_cloud.go +++ b/ovh/types_cloud.go @@ -55,15 +55,20 @@ type CloudProjectGatewayResponse struct { } type CloudProjectOperationResponse struct { - Id string `json:"id"` - Action string `json:"action"` - CreateAt string `json:"createdAt"` - StartedAt string `json:"startedAt"` - CompletedAt *string `json:"completedAt"` - Progress int `json:"progress"` - Regions []string `json:"regions"` - ResourceId *string `json:"resourceId"` - Status string `json:"status"` + Id string `json:"id"` + Action string `json:"action"` + CreateAt string `json:"createdAt"` + StartedAt string `json:"startedAt"` + CompletedAt *string `json:"completedAt"` + Progress int `json:"progress"` + Regions []string `json:"regions"` + ResourceId *string `json:"resourceId"` + Status string `json:"status"` + SubOperations []CloudProjectSubOperation `json:"subOperations"` +} + +type CloudProjectSubOperation struct { + ResourceId *string `json:"resourceId"` } func waitForCloudProjectOperation(ctx context.Context, c *ovh.Client, serviceName, operationId string) (string, error) { @@ -81,6 +86,8 @@ func waitForCloudProjectOperation(ctx context.Context, c *ovh.Client, serviceNam case "completed": if ro.ResourceId != nil { resourceID = *ro.ResourceId + } else if len(ro.SubOperations) > 0 && ro.SubOperations[0].ResourceId != nil { + resourceID = *ro.SubOperations[0].ResourceId } return nil default: diff --git a/ovh/types_cloud_project_instance.go b/ovh/types_cloud_project_instance.go new file mode 100644 index 00000000..4160d216 --- /dev/null +++ b/ovh/types_cloud_project_instance.go @@ -0,0 +1,244 @@ +package ovh + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/ovh/terraform-provider-ovh/ovh/helpers" +) + +type AutoBackup struct { + Cron string `json:"cron"` + Rotation int `json:"rotation"` +} + +type Flavor struct { + FlavorId string `json:"id"` +} + +type BootFrom struct { + ImageId *string `json:"imageId,omitempty"` + VolumeId *string `json:"volumeId,omitempty"` +} + +type Group struct { + GroupId string `json:"id"` +} + +type SshKey struct { + Name string `json:"name"` +} + +type SshKeyCreate struct { + Name string `json:"name"` + PublicKey string `json:"publicKey"` +} + +type Network struct { + Public bool `json:"public"` +} + +type CloudProjectInstanceCreateOpts struct { + AutoBackup *AutoBackup `json:"autobackup,omitempty"` + BillingPeriod string `json:"billingPeriod"` + BootFrom *BootFrom `json:"bootFrom,omitempty"` + Bulk int `json:"bulk"` + Flavor *Flavor `json:"flavor,omitempty"` + Group *Group `json:"group,omitempty"` + Name string `json:"name"` + SshKey *SshKey `json:"sshKey,omitempty"` + SshKeyCreate *SshKeyCreate `json:"sshKeyCreate,omitempty"` + UserData *string `json:"userData,omitempty"` + Network *Network `json:"network,omitempty"` +} + +type Address struct { + Ip *string `json:"ip"` + Version *int `json:"version"` +} + +type AttachedVolume struct { + Id string `json:"id"` +} + +type CloudProjectInstanceResponse struct { + Addresses []Address `json:"addresses"` + AttachedVolumes []AttachedVolume `json:"attachedVolumes"` + FlavorId string `json:"flavorId"` + FlavorName string `json:"flavorName"` + Id string `json:"id"` + ImageId string `json:"imageId"` + Name string `json:"name"` + Region string `json:"region"` + SshKey string `json:"sshKey"` + TaskState string `json:"taskState"` +} + +func (v CloudProjectInstanceResponse) ToMap() map[string]interface{} { + obj := make(map[string]interface{}) + obj["flavor_id"] = v.FlavorId + obj["flavor_name"] = v.FlavorName + obj["image_id"] = v.ImageId + obj["id"] = v.Id + obj["name"] = v.Name + obj["ssh_key"] = v.SshKey + obj["task_state"] = v.TaskState + + addresses := make([]map[string]interface{}, 0, len(v.Addresses)) + for i := range v.Addresses { + address := make(map[string]interface{}) + address["ip"] = v.Addresses[i].Ip + address["version"] = v.Addresses[i].Version + addresses = append(addresses, address) + } + obj["addresses"] = addresses + + attachedVolumes := make([]map[string]interface{}, 0, len(v.AttachedVolumes)) + for i := range v.AttachedVolumes { + attachedVolume := make(map[string]interface{}) + attachedVolume["id"] = v.AttachedVolumes[i].Id + attachedVolumes = append(attachedVolumes, attachedVolume) + } + + obj["attached_volumes"] = attachedVolumes + + return obj +} + +func (a Address) ToMap() map[string]interface{} { + obj := make(map[string]interface{}) + obj["ip"] = a.Ip + obj["version"] = a.Version + return obj +} + +func (a AttachedVolume) ToMap() map[string]interface{} { + obj := make(map[string]interface{}) + obj["attached_volumes"] = a.Id + return obj +} + +type CloudProjectInstanceResponseList struct { + Id string `json:"id"` + Name string `json:"name"` +} + +func GetFlavorId(i interface{}) *Flavor { + if i == nil { + return nil + } + flavorId := Flavor{} + flavorSet := i.(*schema.Set).List() + for _, flavor := range flavorSet { + mapping := flavor.(map[string]interface{}) + flavorId.FlavorId = mapping["flavor_id"].(string) + } + return &flavorId +} + +func GetAutoBackup(i interface{}) *AutoBackup { + if i == nil { + return nil + } + autoBackupOut := AutoBackup{} + + autoBackupSet := i.(*schema.Set).List() + if len(autoBackupSet) == 0 { + return nil + } + for _, autoBackup := range autoBackupSet { + mapping := autoBackup.(map[string]interface{}) + autoBackupOut.Cron = mapping["cron"].(string) + autoBackupOut.Rotation = mapping["rotation"].(int) + } + return &autoBackupOut +} + +func GetBootFrom(i interface{}) *BootFrom { + if i == nil { + return nil + } + bootFromOutput := BootFrom{} + + bootFromSet := i.(*schema.Set).List() + for _, bootFrom := range bootFromSet { + mapping := bootFrom.(map[string]interface{}) + bootFromOutput.ImageId = helpers.GetNilStringPointerFromData(mapping, "image_id") + bootFromOutput.VolumeId = helpers.GetNilStringPointerFromData(mapping, "volume_id") + } + + return &bootFromOutput +} + +func GetGroup(i interface{}) *Group { + if i == nil { + return nil + } + groupOut := Group{} + + groupSet := i.(*schema.Set).List() + for _, group := range groupSet { + mapping := group.(map[string]interface{}) + groupOut.GroupId = mapping["id"].(string) + } + return &groupOut +} + +func GetSshKey(i interface{}) *SshKey { + if i == nil { + return nil + } + sshOutput := SshKey{} + + sshSet := i.(*schema.Set).List() + for _, ssh := range sshSet { + mapping := ssh.(map[string]interface{}) + sshOutput.Name = mapping["name"].(string) + } + + return &sshOutput +} + +func GetSshKeyCreate(i interface{}) *SshKeyCreate { + if i == nil { + return nil + } + sshCreateOutput := SshKeyCreate{} + + sshCreateSet := i.(*schema.Set).List() + if len(sshCreateSet) == 0 { + return nil + } + for _, ssh := range sshCreateSet { + mapping := ssh.(map[string]interface{}) + sshCreateOutput.Name = mapping["name"].(string) + sshCreateOutput.Name = mapping["public_key"].(string) + } + + return &sshCreateOutput +} + +func GetNetwork(i interface{}) *Network { + if i == nil { + return nil + } + networkOutput := Network{} + + networkSet := i.(*schema.Set).List() + for _, network := range networkSet { + mapping := network.(map[string]interface{}) + networkOutput.Public = mapping["public"].(bool) + } + return &networkOutput +} + +func (cpir *CloudProjectInstanceCreateOpts) FromResource(d *schema.ResourceData) { + cpir.Flavor = GetFlavorId(d.Get("flavor")) + cpir.AutoBackup = GetAutoBackup(d.Get("auto_backup")) + cpir.BootFrom = GetBootFrom(d.Get("boot_from")) + cpir.Group = GetGroup(d.Get("group")) + cpir.SshKey = GetSshKey(d.Get("ssh_key")) + cpir.SshKeyCreate = GetSshKeyCreate(d.Get("ssh_key_create")) + cpir.Network = GetNetwork(d.Get("network")) + cpir.BillingPeriod = d.Get("billing_period").(string) + cpir.Name = d.Get("name").(string) + cpir.UserData = helpers.GetNilStringPointerFromData(d, "user_data") +} diff --git a/website/docs/d/cloud_project_instance.markdown b/website/docs/d/cloud_project_instance.markdown new file mode 100644 index 00000000..d05b3168 --- /dev/null +++ b/website/docs/d/cloud_project_instance.markdown @@ -0,0 +1,45 @@ +--- +subcategory : "Cloud Project" +--- + +# ovh_cloud_project_instance +**This datasource uses a Beta API** +Use this data source to get the instance of a public cloud project. + +## Example Usage + +To get information of an instance: + +```hcl +data "ovh_cloud_project_instance" "instance" { + service_name = "YYYY" + region = "XXXX" + instance_id = "ZZZZZ" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service_name` - (Required) The id of the public cloud project. If omitted, + the `OVH_CLOUD_PROJECT_SERVICE` environment variable is used +* `region` - (Required) Instance region +* `instance_id` - (Required) Instance id + +## Attributes Reference + +The following attributes are exported: + +* `addresses` - Instance IP addresses + * `ip` - IP address + * `version` - IP version +* `attached_volumes` - Volumes attached to the instance + * `id` - Volume id +* `flavor_id` - Flavor id +* `flavor_name` - Flavor name +* `id` - Instance id +* `name` - Instance name +* `image_id` - Image id +* `task_state` - Instance task state +* `ssh_key` - SSH Keypair diff --git a/website/docs/d/cloud_project_instances.markdown b/website/docs/d/cloud_project_instances.markdown new file mode 100644 index 00000000..49a9ba61 --- /dev/null +++ b/website/docs/d/cloud_project_instances.markdown @@ -0,0 +1,45 @@ +--- +subcategory : "Cloud Project" +--- + +# ovh_cloud_project_instances + +**This datasource uses a Beta API** + +Use this data source to get the list of instances in a region of a public cloud project. + +## Example Usage + +To list your instances: + +```hcl +data "ovh_cloud_project_instances" "instance" { + service_name = "YYYY" + region = "XXXX" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service_name` - (Required) The id of the public cloud project. If omitted, + the `OVH_CLOUD_PROJECT_SERVICE` environment variable is used. +* `region` - (Required) Instance region. + +## Attributes Reference + +The following attributes are exported: +* `instances` - List of instances + * `addresses` - Instance IP addresses + * `ip` - IP address + * `version` - IP version + * `attached_volumes` - Volumes attached to the instance + * `id` - Volume id + * `flavor_id` - Flavor id + * `flavor_name` - Flavor name + * `id` - Instance id + * `name` - Instance name + * `image_id` - Image id + * `task_state` - Instance task state + * `ssh_key` - SSH Keypair diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 2ff2e93e..c9259741 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -262,11 +262,23 @@ variables must also be set: * `OVH_CLOUD_PROJECT_FAILOVER_IP_ROUTED_TO_2_TEST` - The GUID of a secondary instance to which failover IP addresses can be attached. There must be 2 as associations can only be updated not removed. To test effectively, the failover ip address must be moved between instances +* `OVH_CLOUD_PROJECT_FLAVOR_ID_TEST` - The ID of the Flavor to test + +* `OVH_CLOUD_PROJECT_IMAGE_ID_TEST` - The ID of the image to test + +* `OVH_CLOUD_PROJECT_INSTANCE_ID_TEST` - The ID of the instance to test + +* `OVH_CLOUD_PROJECT_INSTANCE_NAME_TEST` - The Name of the instance to test + * `OVH_CLOUD_PROJECT_KUBE_REGION_TEST` - The region of your public cloud kubernetes project. * `OVH_CLOUD_PROJECT_KUBE_VERSION_TEST` - The version of your public cloud kubernetes project. * `OVH_CLOUD_PROJECT_KUBE_PREV_VERSION_TEST` - The previous version of your public cloud kubernetes project. This is used to test upgrade. +* `OVH_CLOUD_PROJECT_REGION_TEST` - The Name of the region to test + +* `OVH_CLOUD_PROJECT_SSH_NAME_TEST` - The Name of the SSH Key to test + * `OVH_DEDICATED_SERVER` - The name of the dedicated server to test dedicated_server_networking resource. * `OVH_NASHA_SERVICE_TEST` - The name of your HA-NAS service. diff --git a/website/docs/r/cloud_project_instance.markdown b/website/docs/r/cloud_project_instance.markdown new file mode 100644 index 00000000..311285ed --- /dev/null +++ b/website/docs/r/cloud_project_instance.markdown @@ -0,0 +1,77 @@ +--- +subcategory : "Cloud Project" +--- + +# ovh_cloud_project_instance + +**This resource uses a Beta API** +Creates an instance associated with a public cloud project. + +## Example Usage + +Create a instance. + +```hcl +resource "ovh_cloud_project_instance" "instance" { + service_name = "XXX" + region = "RRRR" + billing_period = "hourly" + boot_from { + image_id = "UUID" + } + flavor { + flavor_id = "UUID" + } + name = "instance name" + ssh_key { + name = "sshname" + } + network { + public = true + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service_name` - (Required, Forces new resource) The id of the public cloud project. If omitted, + the `OVH_CLOUD_PROJECT_SERVICE` environment variable is used +* `region` - (Required, Forces new resource) Instance region +* `billing_period` - (Required, Forces new resource) Billing period - hourly or monthly +* `network` - (Required, Forces new resource) Create network interfaces + * `public` - (Optional, Forces new resource) Set the new instance as public boolean +* `flavor` - (Required, Forces new resource) Flavor information + * `flavor_id` - (Required, Forces new resource) Flavor ID. Flavors can be retrieved using `GET /cloud/project/{serviceName}/flavor` +* `boot_from` - (Required, Forces new resource) Boot the instance from an image or a volume + * `image_id` - (Mandatory only if volume_id not used, Forces new resource) Instance image id. Images can be retrieved using `GET /cloud/project/{serviceName}/image` + * `volume_id` - (Mandatory only if image_id not used, Forces new resource) Instance volume id +* `group`- (Optional, Forces new resource) Start instance in group + * `group_id` - (Optional, Forces new resource) Group id +* `name` - (Required, Forces new resource) Instance name +* `ssh_key` - (Mandatory only if ssh_key_create not used, Forces new resource) Existing SSH Keypair + * `name` - (Optional, Forces new resource) SSH Keypair name +* `ssh_key_create` - (Mandatory only if ssh_key not used, Forces new resource) Add existing SSH Key pair into your Public Cloud project and link it to the instance + * `name` - (Optional, Forces new resource) SSH Key pair name + * `public_key` - (Optional, Forces new resource) SSH Public key +* `user_data`- (Optional, Forces new resource) Configuration information or scripts to use upon launch +* `auto_backup` - (Optional, Forces new resource) Create an autobackup workflow after instance start up. + * `cron` - (Optional, Forces new resource) Unix cron pattern + * `rotation` - (Optional, Forces new resource) Number of backup to keep + +## Attributes Reference + +The following attributes are exported: + +* `addresses` - Instance IP addresses + * `ip` - IP address + * `version` - IP version +* `attached_volumes` - Volumes attached to the instance + * `id` - Volume id +* `flavor_id` - Flavor id +* `flavor_name` - Flavor name +* `id` - Instance id +* `name` - Instance name +* `image_id` - Image id +* `task_state` - Instance task state diff --git a/website/docs/r/cloud_project_workflow_backup.html.markdown b/website/docs/r/cloud_project_workflow_backup.html.markdown index 1cc066c3..bee0043f 100644 --- a/website/docs/r/cloud_project_workflow_backup.html.markdown +++ b/website/docs/r/cloud_project_workflow_backup.html.markdown @@ -1,5 +1,5 @@ --- -subcategory : "VM Instances" +subcategory : "Cloud Project" --- # ovh_cloud_project_workflow_backup