diff --git a/docs/resources/devops_instance.md b/docs/resources/devops_instance.md new file mode 100644 index 0000000..be950a2 --- /dev/null +++ b/docs/resources/devops_instance.md @@ -0,0 +1,73 @@ +--- +page_title: "NIFCLOUD: nifcloud_devops_instance" +subcategory: "DevOps with GitLab" +description: |- + Provides a DevOps instance resource. +--- + +# nifcloud_devops_instance + +Provides a DevOps instance resource. + +## Example Usage + +```hcl +terraform { + required_providers { + nifcloud = { + source = "nifcloud/nifcloud" + } + } +} + +provider "nifcloud" { + region = "jp-east-1" +} + +resource "nifcloud_devops_instance" "example" { + instance_id = "example" + instance_type = "c-large" + firewall_group_name = nifcloud_devops_firewall_group.example.name + parameter_group_name = nifcloud_devops_parameter_group.example.name + disk_size = 100 + availability_zone = "east-11" + initial_root_password = "initialroo00ootpassword" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `availability_zone` - (Optional) The availability zone for the DevOps instance. +* `container_registry_bucket_name` - (Optional) The name of the bucket to put container registry objects. +* `description` - (Optional) Description of the DevOps instance. +* `disk_size` - (Required) The allocated storage in gigabytes. +* `firewall_group_name` - (Required) The name of the DevOps firewall group to associate. +* `initial_root_password` - (Required) Initial password for the root user. +* `instance_id` - (Required) The name of the DevOps instance. +* `instance_type` - (Required) The instance type of the DevOps instance. +* `lfs_bucket_name` - (Optional) The name of the bucket to put LFS objects. +* `network_id` - (Optional, but required if `private_address` is provided) The ID of private lan. +* `object_storage_account` - (Optional, but required if `object_storage_region` is provided) The account name of the object storage service. +* `object_storage_region` - (Optional, but required if `object_storage_account` is provided) The region where the bucket exists. +* `packages_bucket_name` - (Optional) The name of the bucket to put packages. +* `parameter_group_name` - (Required) The name of the DevOps parameter group to associate. +* `private_address` - (Optional, but required if `network_id` is provided) Private IP address for the DevOps instance. +* `to` - (Optional) Mail address where alerts are sent. + +## Attribute Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `gitlab_url` - URL for GitLab. +* `registry_url` - URL for GitLab container registry. +* `public_ip_address` - Public IP address for the DevOps instance. + +## Import + +nifcloud_devops_instance can be imported using the `parameter corresponding to id`, e.g. + +``` +$ terraform import nifcloud_devops_instance.example foo +``` diff --git a/examples/devops_instance/main.tf b/examples/devops_instance/main.tf new file mode 100644 index 0000000..2dcfd62 --- /dev/null +++ b/examples/devops_instance/main.tf @@ -0,0 +1,31 @@ +terraform { + required_providers { + nifcloud = { + source = "nifcloud/nifcloud" + } + } +} + +provider "nifcloud" { + region = "jp-east-1" +} + +resource "nifcloud_devops_instance" "example" { + instance_id = "example" + instance_type = "c-large" + firewall_group_name = nifcloud_devops_firewall_group.example.name + parameter_group_name = nifcloud_devops_parameter_group.example.name + disk_size = 100 + availability_zone = "east-11" + initial_root_password = "initialroo00ootpassword" + to = "email@example.com" +} + +resource "nifcloud_devops_firewall_group" "example" { + name = "example" + availability_zone = "east-11" +} + +resource "nifcloud_devops_parameter_group" "example" { + name = "example" +} diff --git a/nifcloud/acc/devops_firewall_group_test.go b/nifcloud/acc/devops_firewall_group_test.go index 2cc26b8..21ec813 100644 --- a/nifcloud/acc/devops_firewall_group_test.go +++ b/nifcloud/acc/devops_firewall_group_test.go @@ -23,9 +23,9 @@ func init() { resource.AddTestSweepers("nifcloud_devops_firewall_group", &resource.Sweeper{ Name: "nifcloud_devops_firewall_group", F: testSweepDevOpsFirewallGroup, - // Dependencies: []string{ - // "nifcloud_devops_instance", - // }, + Dependencies: []string{ + "nifcloud_devops_instance", + }, }) } diff --git a/nifcloud/acc/devops_instance_test.go b/nifcloud/acc/devops_instance_test.go new file mode 100644 index 0000000..00ff84d --- /dev/null +++ b/nifcloud/acc/devops_instance_test.go @@ -0,0 +1,274 @@ +package acc + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "testing" + + "github.com/aws/smithy-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/nifcloud/nifcloud-sdk-go/nifcloud" + "github.com/nifcloud/nifcloud-sdk-go/service/devops" + "github.com/nifcloud/nifcloud-sdk-go/service/devops/types" + "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/client" + "golang.org/x/sync/errgroup" +) + +func init() { + resource.AddTestSweepers("nifcloud_devops_instance", &resource.Sweeper{ + Name: "nifcloud_devops_instance", + F: testSweepDevOpsInstance, + }) +} + +func TestAcc_DevOpsInstance(t *testing.T) { + var instance types.Instance + + resourceName := "nifcloud_devops_instance.basic" + randName := prefix + acctest.RandString(10) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactory, + CheckDestroy: testAccDevOpsInstanceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDevOpsInstance(t, "testdata/devops_instance.tf", randName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDevOpsInstanceExists(resourceName, &instance), + testAccCheckDevOpsInstanceValues(&instance, randName), + resource.TestCheckResourceAttr(resourceName, "instance_id", randName), + resource.TestCheckResourceAttr(resourceName, "instance_type", "c-large"), + resource.TestCheckResourceAttr(resourceName, "firewall_group_name", randName), + resource.TestCheckResourceAttr(resourceName, "parameter_group_name", randName), + resource.TestCheckResourceAttr(resourceName, "disk_size", "100"), + resource.TestCheckResourceAttr(resourceName, "availability_zone", "east-14"), + resource.TestCheckResourceAttr(resourceName, "description", "tfacc-memo"), + resource.TestCheckResourceAttr(resourceName, "to", "email@example.com"), + resource.TestCheckResourceAttr(resourceName, "gitlab_url", "https://"+randName+".jp-east-1.gitlab.devops.nifcloud.com"), + resource.TestCheckResourceAttr(resourceName, "registry_url", "https://registry-"+randName+".jp-east-1.gitlab.devops.nifcloud.com"), + ), + }, + { + Config: testAccDevOpsInstance(t, "testdata/devops_instance_update.tf", randName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDevOpsInstanceExists(resourceName, &instance), + testAccCheckDevOpsInstanceValuesUpdated(&instance, randName), + resource.TestCheckResourceAttr(resourceName, "instance_id", randName), + resource.TestCheckResourceAttr(resourceName, "instance_type", "e-large"), + resource.TestCheckResourceAttr(resourceName, "firewall_group_name", randName+"-upd"), + resource.TestCheckResourceAttr(resourceName, "parameter_group_name", randName), + resource.TestCheckResourceAttr(resourceName, "disk_size", "300"), + resource.TestCheckResourceAttr(resourceName, "availability_zone", "east-14"), + resource.TestCheckResourceAttr(resourceName, "description", "tfacc-memo-upd"), + resource.TestCheckResourceAttr(resourceName, "to", "email-upd@example.com"), + resource.TestCheckResourceAttr(resourceName, "gitlab_url", "https://"+randName+".jp-east-1.gitlab.devops.nifcloud.com"), + resource.TestCheckResourceAttr(resourceName, "registry_url", "https://registry-"+randName+".jp-east-1.gitlab.devops.nifcloud.com"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "initial_root_password", + }, + }, + }, + }) +} + +func testAccDevOpsInstance(t *testing.T, fileName, rName string) string { + b, err := os.ReadFile(fileName) + if err != nil { + t.Fatal(err) + } + return fmt.Sprintf(string(b), rName, rName, rName, rName) +} + +func testAccCheckDevOpsInstanceExists(n string, instance *types.Instance) resource.TestCheckFunc { + return func(s *terraform.State) error { + saved, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("no devops instance resource: %s", n) + } + + if saved.Primary.ID == "" { + return fmt.Errorf("no devops instance id is set") + } + + svc := testAccProvider.Meta().(*client.Client).DevOps + res, err := svc.GetInstance(context.Background(), &devops.GetInstanceInput{ + InstanceId: nifcloud.String(saved.Primary.ID), + }) + if err != nil { + return err + } + + if res.Instance == nil { + return fmt.Errorf("devops instance is not found in cloud: %s", saved.Primary.ID) + } + + if nifcloud.ToString(res.Instance.InstanceId) != saved.Primary.ID { + return fmt.Errorf("devops instance is not found in cloud: %s", saved.Primary.ID) + } + + *instance = *res.Instance + + return nil + } +} + +func testAccCheckDevOpsInstanceValues(instance *types.Instance, rName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if nifcloud.ToString(instance.InstanceId) != rName { + return fmt.Errorf("bad instance id state, expected \"%s\", got: %#v", rName, nifcloud.ToString(instance.InstanceId)) + } + + if nifcloud.ToString(instance.InstanceType) != "c-large" { + return fmt.Errorf("bad instance_type state, expected \"c-large\", got: %#v", nifcloud.ToString(instance.InstanceType)) + } + + if nifcloud.ToString(instance.FirewallGroupName) != rName { + return fmt.Errorf("bad firewall_group_name state, expected \"%s\", got: %#v", rName, nifcloud.ToString(instance.FirewallGroupName)) + } + + if nifcloud.ToString(instance.ParameterGroupName) != rName { + return fmt.Errorf("bad parameter_group_name state, expected \"%s\", got: %#v", rName, nifcloud.ToString(instance.ParameterGroupName)) + } + + if nifcloud.ToInt32(instance.DiskSize) != int32(100) { + return fmt.Errorf("bad disk_size state, expected 100, got: %#v", nifcloud.ToInt32(instance.DiskSize)) + } + + if nifcloud.ToString(instance.AvailabilityZone) != "east-14" { + return fmt.Errorf("bad availability_zone state, expected \"east-14\", got: %#v", nifcloud.ToString(instance.AvailabilityZone)) + } + + if nifcloud.ToString(instance.Description) != "tfacc-memo" { + return fmt.Errorf("bad description state, expected \"tfacc-memo\", got: %#v", nifcloud.ToString(instance.Description)) + } + + if nifcloud.ToString(instance.To) != "email@example.com" { + return fmt.Errorf("bad to state, expected \"email@example.com\", got: %#v", nifcloud.ToString(instance.To)) + } + + if nifcloud.ToString(instance.GitlabUrl) != "https://"+rName+".jp-east-1.gitlab.devops.nifcloud.com" { + return fmt.Errorf("bad gitlab_url state, expected \"https://%s.jp-east-1.gitlab.devops.nifcloud.com\", got: %#v", rName, nifcloud.ToString(instance.GitlabUrl)) + } + + if nifcloud.ToString(instance.RegistryUrl) != "https://registry-"+rName+".jp-east-1.gitlab.devops.nifcloud.com" { + return fmt.Errorf("bad registry_url state, expected \"https://registry-%s.jp-east-1.gitlab.devops.nifcloud.com\", got: %#v", rName, nifcloud.ToString(instance.RegistryUrl)) + } + + return nil + } +} + +func testAccCheckDevOpsInstanceValuesUpdated(instance *types.Instance, rName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if nifcloud.ToString(instance.InstanceId) != rName { + return fmt.Errorf("bad instance id state, expected \"%s\", got: %#v", rName, nifcloud.ToString(instance.InstanceId)) + } + + if nifcloud.ToString(instance.InstanceType) != "e-large" { + return fmt.Errorf("bad instance_type state, expected \"e-large\", got: %#v", nifcloud.ToString(instance.InstanceType)) + } + + if nifcloud.ToString(instance.FirewallGroupName) != rName+"-upd" { + return fmt.Errorf("bad firewall_group_name state, expected \"%s\", got: %#v", rName+"-upd", nifcloud.ToString(instance.FirewallGroupName)) + } + + if nifcloud.ToString(instance.ParameterGroupName) != rName { + return fmt.Errorf("bad parameter_group_name state, expected \"%s\", got: %#v", rName, nifcloud.ToString(instance.ParameterGroupName)) + } + + if nifcloud.ToInt32(instance.DiskSize) != int32(300) { + return fmt.Errorf("bad disk_size state, expected 300, got: %#v", nifcloud.ToInt32(instance.DiskSize)) + } + + if nifcloud.ToString(instance.AvailabilityZone) != "east-14" { + return fmt.Errorf("bad availability_zone state, expected \"east-14\", got: %#v", nifcloud.ToString(instance.AvailabilityZone)) + } + + if nifcloud.ToString(instance.Description) != "tfacc-memo-upd" { + return fmt.Errorf("bad description state, expected \"tfacc-memo-upd\", got: %#v", nifcloud.ToString(instance.Description)) + } + + if nifcloud.ToString(instance.To) != "email-upd@example.com" { + return fmt.Errorf("bad to state, expected \"email-upd@example.com\", got: %#v", nifcloud.ToString(instance.To)) + } + + if nifcloud.ToString(instance.GitlabUrl) != "https://"+rName+".jp-east-1.gitlab.devops.nifcloud.com" { + return fmt.Errorf("bad gitlab_url state, expected \"https://%s.jp-east-1.gitlab.devops.nifcloud.com\", got: %#v", rName, nifcloud.ToString(instance.GitlabUrl)) + } + + if nifcloud.ToString(instance.RegistryUrl) != "https://registry-"+rName+".jp-east-1.gitlab.devops.nifcloud.com" { + return fmt.Errorf("bad registry_url state, expected \"https://registry-%s.jp-east-1.gitlab.devops.nifcloud.com\", got: %#v", rName, nifcloud.ToString(instance.RegistryUrl)) + } + + return nil + } +} + +func testAccDevOpsInstanceResourceDestroy(s *terraform.State) error { + svc := testAccProvider.Meta().(*client.Client).DevOps + + for _, rs := range s.RootModule().Resources { + if rs.Type != "nifcloud_devops_instance" { + continue + } + + _, err := svc.GetInstance(context.Background(), &devops.GetInstanceInput{ + InstanceId: nifcloud.String(rs.Primary.ID), + }) + + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) && awsErr.ErrorCode() == "Client.InvalidParameterNotFound.Instance" { + return nil + } + return fmt.Errorf("failed GetInstance: %s", err) + } + + return fmt.Errorf("devops instance (%s) still exists", rs.Primary.ID) + } + return nil +} + +func testSweepDevOpsInstance(region string) error { + ctx := context.Background() + svc := sharedClientForRegion(region).DevOps + + res, err := svc.ListInstances(ctx, nil) + if err != nil { + return err + } + + var sweepInstances []string + for _, i := range res.Instances { + if strings.HasPrefix(nifcloud.ToString(i.InstanceId), prefix) { + sweepInstances = append(sweepInstances, nifcloud.ToString(i.InstanceId)) + } + } + + eg, ctx := errgroup.WithContext(ctx) + for _, n := range sweepInstances { + instance := n + eg.Go(func() error { + _, err := svc.DeleteInstance(ctx, &devops.DeleteInstanceInput{ + InstanceId: nifcloud.String(instance), + }) + return err + }) + } + if err := eg.Wait(); err != nil { + return err + } + return nil +} diff --git a/nifcloud/acc/devops_parameter_group_test.go b/nifcloud/acc/devops_parameter_group_test.go index 50e9849..6e6d3eb 100644 --- a/nifcloud/acc/devops_parameter_group_test.go +++ b/nifcloud/acc/devops_parameter_group_test.go @@ -23,9 +23,9 @@ func init() { resource.AddTestSweepers("nifcloud_devops_parameter_group", &resource.Sweeper{ Name: "nifcloud_devops_parameter_group", F: testSweepDevOpsParameterGroup, - // Dependencies: []string{ - // "nifcloud_devops_instance", - // }, + Dependencies: []string{ + "nifcloud_devops_instance", + }, }) } diff --git a/nifcloud/acc/testdata/devops_instance.tf b/nifcloud/acc/testdata/devops_instance.tf new file mode 100644 index 0000000..44f0e3e --- /dev/null +++ b/nifcloud/acc/testdata/devops_instance.tf @@ -0,0 +1,29 @@ +provider "nifcloud" { + region = "jp-east-1" +} + +resource "nifcloud_devops_instance" "basic" { + instance_id = "%s" + instance_type = "c-large" + firewall_group_name = nifcloud_devops_firewall_group.basic.name + parameter_group_name = nifcloud_devops_parameter_group.basic.name + disk_size = 100 + availability_zone = "east-14" + description = "tfacc-memo" + initial_root_password = "initialroo00ootpassword" + to = "email@example.com" +} + +resource "nifcloud_devops_firewall_group" "basic" { + name = "%s" + availability_zone = "east-14" +} + +resource "nifcloud_devops_parameter_group" "basic" { + name = "%s" +} + +resource "nifcloud_devops_firewall_group" "upd" { + name = "%s-upd" + availability_zone = "east-14" +} diff --git a/nifcloud/acc/testdata/devops_instance_update.tf b/nifcloud/acc/testdata/devops_instance_update.tf new file mode 100644 index 0000000..1dabaf9 --- /dev/null +++ b/nifcloud/acc/testdata/devops_instance_update.tf @@ -0,0 +1,29 @@ +provider "nifcloud" { + region = "jp-east-1" +} + +resource "nifcloud_devops_instance" "basic" { + instance_id = "%s" + instance_type = "e-large" + firewall_group_name = nifcloud_devops_firewall_group.upd.name + parameter_group_name = nifcloud_devops_parameter_group.basic.name + disk_size = 300 + availability_zone = "east-14" + description = "tfacc-memo-upd" + initial_root_password = "initialroo00ootpassword" + to = "email-upd@example.com" +} + +resource "nifcloud_devops_firewall_group" "basic" { + name = "%s" + availability_zone = "east-14" +} + +resource "nifcloud_devops_parameter_group" "basic" { + name = "%s" +} + +resource "nifcloud_devops_firewall_group" "upd" { + name = "%s-upd" + availability_zone = "east-14" +} diff --git a/nifcloud/provider.go b/nifcloud/provider.go index 147f7d2..f0ce1a1 100644 --- a/nifcloud/provider.go +++ b/nifcloud/provider.go @@ -12,6 +12,7 @@ import ( "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/resources/computing/separateinstancerule" "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/resources/computing/volume" "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/resources/devops/devopsfirewallgroup" + "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/resources/devops/devopsinstance" "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/resources/devops/devopsparametergroup" "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/resources/dns/record" "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/resources/dns/zone" @@ -127,6 +128,7 @@ func Provider() *schema.Provider { "nifcloud_web_proxy": webproxy.New(), "nifcloud_separate_instance_rule": separateinstancerule.New(), "nifcloud_storage_bucket": bucket.New(), + "nifcloud_devops_instance": devopsinstance.New(), "nifcloud_devops_parameter_group": devopsparametergroup.New(), "nifcloud_devops_firewall_group": devopsfirewallgroup.New(), }, diff --git a/nifcloud/resources/devops/devopsinstance/create.go b/nifcloud/resources/devops/devopsinstance/create.go new file mode 100644 index 0000000..1c4cb51 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/create.go @@ -0,0 +1,31 @@ +package devopsinstance + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/nifcloud-sdk-go/nifcloud" + "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/client" +) + +func createInstance(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + svc := meta.(*client.Client).DevOps + + input := expandCreateInstanceInput(d) + + res, err := svc.CreateInstance(ctx, input) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to create a DevOps instance: %s", err)) + } + + d.SetId(nifcloud.ToString(res.Instance.InstanceId)) + + err = waitUntilInstanceRunning(ctx, d, svc) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to wait for the DevOps instance to become ready: %s", err)) + } + + return updateInstance(ctx, d, meta) +} diff --git a/nifcloud/resources/devops/devopsinstance/delete.go b/nifcloud/resources/devops/devopsinstance/delete.go new file mode 100644 index 0000000..db55541 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/delete.go @@ -0,0 +1,29 @@ +package devopsinstance + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/client" +) + +func deleteInstance(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + svc := meta.(*client.Client).DevOps + + input := expandDeleteInstanceInput(d) + + if _, err := svc.DeleteInstance(ctx, input); err != nil { + return diag.FromErr(fmt.Errorf("failed to delete a DevOps instance: %s", err)) + } + + err := waitUntilInstanceDeleted(ctx, d, svc) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to wait until the DevOps instance is deleted: %s", err)) + } + + d.SetId("") + + return nil +} diff --git a/nifcloud/resources/devops/devopsinstance/expander.go b/nifcloud/resources/devops/devopsinstance/expander.go new file mode 100644 index 0000000..d2064a6 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/expander.go @@ -0,0 +1,92 @@ +package devopsinstance + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/nifcloud-sdk-go/nifcloud" + "github.com/nifcloud/nifcloud-sdk-go/service/devops" + "github.com/nifcloud/nifcloud-sdk-go/service/devops/types" +) + +func expandCreateInstanceInput(d *schema.ResourceData) *devops.CreateInstanceInput { + input := &devops.CreateInstanceInput{ + InstanceId: nifcloud.String(d.Get("instance_id").(string)), + InstanceType: types.InstanceTypeOfCreateInstanceRequest(d.Get("instance_type").(string)), + FirewallGroupName: nifcloud.String(d.Get("firewall_group_name").(string)), + ParameterGroupName: nifcloud.String(d.Get("parameter_group_name").(string)), + DiskSize: nifcloud.Int32(int32(d.Get("disk_size").(int))), + AvailabilityZone: types.AvailabilityZoneOfCreateInstanceRequest(d.Get("availability_zone").(string)), + Description: nifcloud.String(d.Get("description").(string)), + InitialRootPassword: nifcloud.String(d.Get("initial_root_password").(string)), + NetworkConfig: expandNetworkConfig(d), + ObjectStorageConfig: expandObjectStorageConfig(d), + } + return input +} + +func expandUpdateInstanceInput(d *schema.ResourceData) *devops.UpdateInstanceInput { + return &devops.UpdateInstanceInput{ + InstanceId: nifcloud.String(d.Id()), + InstanceType: types.InstanceTypeOfUpdateInstanceRequest(d.Get("instance_type").(string)), + FirewallGroupName: nifcloud.String(d.Get("firewall_group_name").(string)), + Description: nifcloud.String(d.Get("description").(string)), + } +} + +func expandGetInstanceInput(d *schema.ResourceData) *devops.GetInstanceInput { + return &devops.GetInstanceInput{ + InstanceId: nifcloud.String(d.Id()), + } +} + +func expandDeleteInstanceInput(d *schema.ResourceData) *devops.DeleteInstanceInput { + return &devops.DeleteInstanceInput{ + InstanceId: nifcloud.String(d.Id()), + } +} + +func expandExtendDiskInput(d *schema.ResourceData) *devops.ExtendDiskInput { + return &devops.ExtendDiskInput{InstanceId: nifcloud.String(d.Id())} +} + +func expandSetupAlertInput(d *schema.ResourceData) *devops.SetupAlertInput { + return &devops.SetupAlertInput{ + InstanceId: nifcloud.String(d.Id()), + To: nifcloud.String(d.Get("to").(string)), + } +} + +func expandUpdateNetworkInterfaceInput(d *schema.ResourceData) *devops.UpdateNetworkInterfaceInput { + return &devops.UpdateNetworkInterfaceInput{ + InstanceId: nifcloud.String(d.Id()), + NetworkConfig: expandNetworkConfig(d), + } +} + +func expandNetworkConfig(d *schema.ResourceData) *types.RequestNetworkConfig { + networkId := d.Get("network_id").(string) + privateAddress := d.Get("private_address").(string) + if networkId == "" && privateAddress == "" { + return nil + } + + return &types.RequestNetworkConfig{ + NetworkId: nifcloud.String(d.Get("network_id").(string)), + PrivateAddress: nifcloud.String(d.Get("private_address").(string)), + } +} + +func expandObjectStorageConfig(d *schema.ResourceData) *types.RequestObjectStorageConfig { + if d.Get("object_storage_account").(string) == "" || d.Get("object_storage_region").(string) == "" { + return nil + } + + return &types.RequestObjectStorageConfig{ + Account: nifcloud.String(d.Get("object_storage_account").(string)), + Region: types.RegionOfobjectStorageConfigForCreateInstance(d.Get("object_storage_region").(string)), + RequestBucketUseObjects: &types.RequestBucketUseObjects{ + Lfs: nifcloud.String(d.Get("lfs_bucket_name").(string)), + Packages: nifcloud.String(d.Get("packages_bucket_name").(string)), + ContainerRegistry: nifcloud.String(d.Get("container_registry_bucket_name").(string)), + }, + } +} diff --git a/nifcloud/resources/devops/devopsinstance/expander_test.go b/nifcloud/resources/devops/devopsinstance/expander_test.go new file mode 100644 index 0000000..dc919a4 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/expander_test.go @@ -0,0 +1,314 @@ +package devopsinstance + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/nifcloud-sdk-go/nifcloud" + "github.com/nifcloud/nifcloud-sdk-go/service/devops" + "github.com/nifcloud/nifcloud-sdk-go/service/devops/types" + "github.com/stretchr/testify/assert" +) + +func TestExpandCreateInstanceInput(t *testing.T) { + rd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{ + "instance_id": "test_id", + "instance_type": "c-large", + "firewall_group_name": "test_name_fg", + "parameter_group_name": "test_name_pg", + "disk_size": 100, + "availability_zone": "east-11", + "description": "test_description", + "initial_root_password": "test_password", + "network_id": "test_id_nw", + "private_address": "192.168.1.1/24", + "object_storage_account": "test_account", + "object_storage_region": "test_region", + "lfs_bucket_name": "test_name_lfs", + "packages_bucket_name": "test_name_pkg", + "container_registry_bucket_name": "test_name_cr", + "to": "test@mail.com", + }) + + tests := []struct { + name string + args *schema.ResourceData + want *devops.CreateInstanceInput + }{ + { + name: "expands the resource data", + args: rd, + want: &devops.CreateInstanceInput{ + InstanceId: nifcloud.String("test_id"), + InstanceType: types.InstanceTypeOfCreateInstanceRequest("c-large"), + FirewallGroupName: nifcloud.String("test_name_fg"), + ParameterGroupName: nifcloud.String("test_name_pg"), + DiskSize: nifcloud.Int32(int32(100)), + AvailabilityZone: types.AvailabilityZoneOfCreateInstanceRequest("east-11"), + Description: nifcloud.String("test_description"), + InitialRootPassword: nifcloud.String("test_password"), + NetworkConfig: &types.RequestNetworkConfig{ + NetworkId: nifcloud.String("test_id_nw"), + PrivateAddress: nifcloud.String("192.168.1.1/24"), + }, + ObjectStorageConfig: &types.RequestObjectStorageConfig{ + Account: nifcloud.String("test_account"), + Region: types.RegionOfobjectStorageConfigForCreateInstance("test_region"), + RequestBucketUseObjects: &types.RequestBucketUseObjects{ + Lfs: nifcloud.String("test_name_lfs"), + Packages: nifcloud.String("test_name_pkg"), + ContainerRegistry: nifcloud.String("test_name_cr"), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandCreateInstanceInput(tt.args) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExpandUpdateInstanceInput(t *testing.T) { + rd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{ + "instance_id": "test_id", + "instance_type": "c-large", + "firewall_group_name": "test_name_fg", + "parameter_group_name": "test_name_pg", + "disk_size": 100, + "availability_zone": "east-11", + "description": "test_description", + "initial_root_password": "test_password", + "network_id": "test_id_nw", + "private_address": "192.168.1.1/24", + "object_storage_account": "test_account", + "object_storage_region": "test_region", + "lfs_bucket_name": "test_name_lfs", + "packages_bucket_name": "test_name_pkg", + "container_registry_bucket_name": "test_name_cr", + "to": "test@mail.com", + }) + rd.SetId("test_id") + + tests := []struct { + name string + args *schema.ResourceData + want *devops.UpdateInstanceInput + }{ + { + name: "expands the resource data", + args: rd, + want: &devops.UpdateInstanceInput{ + InstanceId: nifcloud.String("test_id"), + InstanceType: types.InstanceTypeOfUpdateInstanceRequest("c-large"), + FirewallGroupName: nifcloud.String("test_name_fg"), + Description: nifcloud.String("test_description"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandUpdateInstanceInput(tt.args) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExpandGetInstanceInput(t *testing.T) { + rd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{}) + rd.SetId("test_id") + + tests := []struct { + name string + args *schema.ResourceData + want *devops.GetInstanceInput + }{ + { + name: "expands the resource data", + args: rd, + want: &devops.GetInstanceInput{ + InstanceId: nifcloud.String("test_id"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandGetInstanceInput(tt.args) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExpandDeleteInstanceInput(t *testing.T) { + rd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{}) + rd.SetId("test_id") + + tests := []struct { + name string + args *schema.ResourceData + want *devops.DeleteInstanceInput + }{ + { + name: "expands the resource data", + args: rd, + want: &devops.DeleteInstanceInput{ + InstanceId: nifcloud.String("test_id"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandDeleteInstanceInput(tt.args) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExpandExtendDiskInput(t *testing.T) { + rd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{}) + rd.SetId("test_id") + + tests := []struct { + name string + args *schema.ResourceData + want *devops.ExtendDiskInput + }{ + { + name: "expands the resource data", + args: rd, + want: &devops.ExtendDiskInput{ + InstanceId: nifcloud.String("test_id"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandExtendDiskInput(tt.args) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExpandSetupAlertInput(t *testing.T) { + rd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{ + "instance_id": "test_id", + "instance_type": "c-large", + "firewall_group_name": "test_name_fg", + "parameter_group_name": "test_name_pg", + "disk_size": 100, + "availability_zone": "east-11", + "description": "test_description", + "initial_root_password": "test_password", + "network_id": "test_id_nw", + "private_address": "192.168.1.1/24", + "object_storage_account": "test_account", + "object_storage_region": "test_region", + "lfs_bucket_name": "test_name_lfs", + "packages_bucket_name": "test_name_pkg", + "container_registry_bucket_name": "test_name_cr", + "to": "test@mail.com", + }) + rd.SetId("test_id") + + tests := []struct { + name string + args *schema.ResourceData + want *devops.SetupAlertInput + }{ + { + name: "expands the resource data", + args: rd, + want: &devops.SetupAlertInput{ + InstanceId: nifcloud.String("test_id"), + To: nifcloud.String("test@mail.com"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandSetupAlertInput(tt.args) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExpandUpdateNetworkInterfaceInput(t *testing.T) { + rd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{ + "instance_id": "test_id", + "instance_type": "c-large", + "firewall_group_name": "test_name_fg", + "parameter_group_name": "test_name_pg", + "disk_size": 100, + "availability_zone": "east-11", + "description": "test_description", + "initial_root_password": "test_password", + "network_id": "test_id_nw", + "private_address": "192.168.1.1/24", + "object_storage_account": "test_account", + "object_storage_region": "test_region", + "lfs_bucket_name": "test_name_lfs", + "packages_bucket_name": "test_name_pkg", + "container_registry_bucket_name": "test_name_cr", + "to": "test@mail.com", + }) + rd.SetId("test_id") + + noNetworkConfigRd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{ + "instance_id": "test_id", + "instance_type": "c-large", + "firewall_group_name": "test_name_fg", + "parameter_group_name": "test_name_pg", + "disk_size": 100, + "availability_zone": "east-11", + "description": "test_description", + "initial_root_password": "test_password", + "object_storage_account": "test_account", + "object_storage_region": "test_region", + "lfs_bucket_name": "test_name_lfs", + "packages_bucket_name": "test_name_pkg", + "container_registry_bucket_name": "test_name_cr", + "to": "test@mail.com", + }) + noNetworkConfigRd.SetId("test_id") + + tests := []struct { + name string + args *schema.ResourceData + want *devops.UpdateNetworkInterfaceInput + }{ + { + name: "expands the resource data", + args: rd, + want: &devops.UpdateNetworkInterfaceInput{ + InstanceId: nifcloud.String("test_id"), + NetworkConfig: &types.RequestNetworkConfig{ + NetworkId: nifcloud.String("test_id_nw"), + PrivateAddress: nifcloud.String("192.168.1.1/24"), + }, + }, + }, + { + name: "expands the resource data (no network config)", + args: noNetworkConfigRd, + want: &devops.UpdateNetworkInterfaceInput{ + InstanceId: nifcloud.String("test_id"), + NetworkConfig: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandUpdateNetworkInterfaceInput(tt.args) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/nifcloud/resources/devops/devopsinstance/flattener.go b/nifcloud/resources/devops/devopsinstance/flattener.go new file mode 100644 index 0000000..d495613 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/flattener.go @@ -0,0 +1,98 @@ +package devopsinstance + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/nifcloud-sdk-go/nifcloud" + "github.com/nifcloud/nifcloud-sdk-go/service/devops" +) + +func flatten(d *schema.ResourceData, res *devops.GetInstanceOutput) error { + if res == nil || res.Instance == nil { + d.SetId("") + return nil + } + + instance := res.Instance + + if nifcloud.ToString(instance.InstanceId) != d.Id() { + return fmt.Errorf("unable to find the DevOps instance within: %#v", instance) + } + + if err := d.Set("instance_id", instance.InstanceId); err != nil { + return err + } + + if err := d.Set("instance_type", instance.InstanceType); err != nil { + return err + } + + if err := d.Set("firewall_group_name", instance.FirewallGroupName); err != nil { + return err + } + + if err := d.Set("parameter_group_name", instance.ParameterGroupName); err != nil { + return err + } + + if err := d.Set("disk_size", instance.DiskSize); err != nil { + return err + } + + if err := d.Set("availability_zone", instance.AvailabilityZone); err != nil { + return err + } + + if err := d.Set("description", instance.Description); err != nil { + return err + } + + if err := d.Set("network_id", instance.NetworkConfig.NetworkId); err != nil { + return err + } + + if err := d.Set("private_address", instance.NetworkConfig.PrivateAddress); err != nil { + return err + } + + if err := d.Set("object_storage_account", instance.ObjectStorageConfig.Account); err != nil { + return err + } + + if err := d.Set("object_storage_region", instance.ObjectStorageConfig.Region); err != nil { + return err + } + + if instance.ObjectStorageConfig.BucketUseObjects != nil { + if err := d.Set("lfs_bucket_name", instance.ObjectStorageConfig.BucketUseObjects.Lfs); err != nil { + return err + } + + if err := d.Set("packages_bucket_name", instance.ObjectStorageConfig.BucketUseObjects.Packages); err != nil { + return err + } + + if err := d.Set("container_registry_bucket_name", instance.ObjectStorageConfig.BucketUseObjects.ContainerRegistry); err != nil { + return err + } + } + + if err := d.Set("to", instance.To); err != nil { + return err + } + + if err := d.Set("gitlab_url", instance.GitlabUrl); err != nil { + return err + } + + if err := d.Set("registry_url", instance.RegistryUrl); err != nil { + return err + } + + if err := d.Set("public_ip_address", instance.PublicIpAddress); err != nil { + return err + } + + return nil +} diff --git a/nifcloud/resources/devops/devopsinstance/flattener_test.go b/nifcloud/resources/devops/devopsinstance/flattener_test.go new file mode 100644 index 0000000..244ae99 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/flattener_test.go @@ -0,0 +1,115 @@ +package devopsinstance + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/nifcloud-sdk-go/nifcloud" + "github.com/nifcloud/nifcloud-sdk-go/service/devops" + "github.com/nifcloud/nifcloud-sdk-go/service/devops/types" + "github.com/stretchr/testify/assert" +) + +func TestFlatten(t *testing.T) { + rd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{}) + rd.SetId("test_id") + + wantRd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{ + "instance_id": "test_id", + "instance_type": "c-large", + "firewall_group_name": "test_name_fg", + "parameter_group_name": "test_name_pg", + "disk_size": 100, + "availability_zone": "east-11", + "description": "test_description", + "network_id": "test_id_nw", + "private_address": "192.168.1.1/24", + "gitlab_url": "test_url_gl", + "registry_url": "test_url_cr", + "public_ip_address": "198.51.100.1", + "object_storage_account": "test_account", + "object_storage_region": "test_region", + "lfs_bucket_name": "test_name_lfs", + "packages_bucket_name": "test_name_pkg", + "container_registry_bucket_name": "test_name_cr", + "to": "test@mail.com", + }) + wantRd.SetId("test_id") + + wantNotFoundRd := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{}) + + type args struct { + res *devops.GetInstanceOutput + d *schema.ResourceData + } + tests := []struct { + name string + args args + want *schema.ResourceData + }{ + { + name: "flattens the response", + args: args{ + d: rd, + res: &devops.GetInstanceOutput{ + Instance: &types.Instance{ + InstanceId: nifcloud.String("test_id"), + InstanceType: nifcloud.String("c-large"), + FirewallGroupName: nifcloud.String("test_name_fg"), + ParameterGroupName: nifcloud.String("test_name_pg"), + DiskSize: nifcloud.Int32(int32(100)), + AvailabilityZone: nifcloud.String("east-11"), + Description: nifcloud.String("test_description"), + NetworkConfig: &types.NetworkConfig{ + NetworkId: nifcloud.String("test_id_nw"), + PrivateAddress: nifcloud.String("192.168.1.1/24"), + }, + ObjectStorageConfig: &types.ObjectStorageConfig{ + Account: nifcloud.String("test_account"), + Region: nifcloud.String("test_region"), + BucketUseObjects: &types.BucketUseObjects{ + Lfs: nifcloud.String("test_name_lfs"), + Packages: nifcloud.String("test_name_pkg"), + ContainerRegistry: nifcloud.String("test_name_cr"), + }, + }, + To: nifcloud.String("test@mail.com"), + GitlabUrl: nifcloud.String("test_url_gl"), + RegistryUrl: nifcloud.String("test_url_cr"), + PublicIpAddress: nifcloud.String("198.51.100.1"), + }, + }, + }, + want: wantRd, + }, + { + name: "flattens the response even when the resource has been removed externally", + args: args{ + d: wantNotFoundRd, + res: nil, + }, + want: wantNotFoundRd, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := flatten(tt.args.d, tt.args.res) + assert.NoError(t, err) + + wantState := tt.want.State() + if wantState == nil { + tt.want.SetId("some") + wantState = tt.want.State() + } + + gotState := tt.args.d.State() + if gotState == nil { + tt.args.d.SetId("some") + gotState = tt.args.d.State() + } + + assert.Equal(t, wantState.Attributes, gotState.Attributes) + }) + } +} diff --git a/nifcloud/resources/devops/devopsinstance/helper.go b/nifcloud/resources/devops/devopsinstance/helper.go new file mode 100644 index 0000000..41fca66 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/helper.go @@ -0,0 +1,58 @@ +package devopsinstance + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/aws/smithy-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/nifcloud-sdk-go/nifcloud" + "github.com/nifcloud/nifcloud-sdk-go/service/devops" +) + +// waitUntilInstanceRunning waits until the state of the instance become RUNNING. +// DevOps SDK does not provide a waiter. +func waitUntilInstanceRunning(ctx context.Context, d *schema.ResourceData, svc *devops.Client) error { + const timeout = 3600 * time.Second + + err := retry.RetryContext(ctx, timeout, func() *retry.RetryError { + input := expandGetInstanceInput(d) + res, err := svc.GetInstance(ctx, input) + if err != nil { + return retry.NonRetryableError(err) + } + + if nifcloud.ToString(res.Instance.State) == "RUNNING" { + return nil + } + + return retry.RetryableError(fmt.Errorf("expected the DevOps instance was in state RUNNING")) + }) + + return err +} + +// waitUntilInstanceDeleted waits until the state of the instance is deleted. +// DevOps SDK does not provide a waiter. +func waitUntilInstanceDeleted(ctx context.Context, d *schema.ResourceData, svc *devops.Client) error { + const timeout = 200 * time.Second + + err := retry.RetryContext(ctx, timeout, func() *retry.RetryError { + input := expandGetInstanceInput(d) + _, err := svc.GetInstance(ctx, input) + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) && awsErr.ErrorCode() == "Client.InvalidParameterNotFound.Instance" { + return nil + } + return retry.RetryableError(fmt.Errorf("failed to read a DevOps instance: %s", err)) + } + + return retry.RetryableError(fmt.Errorf("expected the DevOps instance was deleted")) + }) + + return err +} diff --git a/nifcloud/resources/devops/devopsinstance/read.go b/nifcloud/resources/devops/devopsinstance/read.go new file mode 100644 index 0000000..b6a9611 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/read.go @@ -0,0 +1,32 @@ +package devopsinstance + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/smithy-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/client" +) + +func readInstance(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + svc := meta.(*client.Client).DevOps + + res, err := svc.GetInstance(ctx, expandGetInstanceInput(d)) + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) && awsErr.ErrorCode() == "Client.InvalidParameterNotFound.Instance" { + d.SetId("") + return nil + } + return diag.FromErr(fmt.Errorf("failed to read a DevOps instance: %s", err)) + } + + if err := flatten(d, res); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/nifcloud/resources/devops/devopsinstance/schema.go b/nifcloud/resources/devops/devopsinstance/schema.go new file mode 100644 index 0000000..7772ac0 --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/schema.go @@ -0,0 +1,177 @@ +package devopsinstance + +import ( + "context" + "fmt" + "regexp" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/validator" +) + +const description = "Provides a DevOps instance resource." + +// New returns the nifcloud_devops_instance resource schema. +func New() *schema.Resource { + return &schema.Resource{ + Description: description, + Schema: newSchema(), + + CreateContext: createInstance, + ReadContext: readInstance, + UpdateContext: updateInstance, + DeleteContext: deleteInstance, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Default: schema.DefaultTimeout(5 * time.Minute), + Create: schema.DefaultTimeout(80 * time.Minute), + Update: schema.DefaultTimeout(80 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + CustomizeDiff: customdiff.ValidateChange( + "disk_size", + func(ctx context.Context, o, n, meta interface{}) error { + if n.(int) < o.(int) { + return fmt.Errorf("new disk size value must be greater than or equal to old value %d", o.(int)) + } + return nil + }, + ), + } +} + +func newSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "instance_id": { + Type: schema.TypeString, + Description: "The name of the DevOps instance.", + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 30), + validation.StringMatch(regexp.MustCompile(`^[0-9a-zA-Z\-]+$`), "Enter a name within 1-30 alphanumeric lowercase characters and hyphens. Hyphens cannot be used at the beginning or end."), + ), + }, + "instance_type": { + Type: schema.TypeString, + Description: "The instance type of the DevOps instance.", + Required: true, + }, + "firewall_group_name": { + Type: schema.TypeString, + Description: "The name of the DevOps firewall group to associate.", + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 63), + validation.StringMatch(regexp.MustCompile(`^[0-9a-zA-Z\-]+$`), "Enter a name within 1-63 alphanumeric lowercase characters and hyphens. Hyphens cannot be used at the beginning or end."), + ), + }, + "parameter_group_name": { + Type: schema.TypeString, + Description: "The name of the DevOps parameter group to associate.", + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 63), + validation.StringMatch(regexp.MustCompile(`^[0-9a-zA-Z\-]+$`), "Enter a name within 1-63 alphanumeric lowercase characters and hyphens. Hyphens cannot be used at the beginning or end."), + ), + }, + "disk_size": { + Type: schema.TypeInt, + Description: "The allocated storage in gigabytes.", + Required: true, + ValidateFunc: validation.All( + validation.IntBetween(100, 400), + validation.IntDivisibleBy(100), + ), + }, + "availability_zone": { + Type: schema.TypeString, + Description: "The availability zone for the DevOps instance.", + Optional: true, + Computed: true, + ForceNew: true, + }, + "description": { + Type: schema.TypeString, + Description: "Description of the DevOps instance.", + Optional: true, + ValidateDiagFunc: validator.StringRuneCountBetween(0, 255), + }, + "initial_root_password": { + Type: schema.TypeString, + Description: "Initial password for the root user.", + Required: true, + Sensitive: true, + ForceNew: true, + }, + "network_id": { + Type: schema.TypeString, + Description: "The ID of private lan.", + Optional: true, + }, + "private_address": { + Type: schema.TypeString, + Description: "Private IP address for the DevOps instance.", + Optional: true, + }, + "object_storage_account": { + Type: schema.TypeString, + Description: "The account name of the object storage service.", + Optional: true, + ForceNew: true, + }, + "object_storage_region": { + Type: schema.TypeString, + Description: "The region where the bucket exists.", + Optional: true, + ForceNew: true, + }, + "lfs_bucket_name": { + Type: schema.TypeString, + Description: "The name of the bucket to put LFS objects.", + Optional: true, + Computed: true, + ForceNew: true, + }, + "packages_bucket_name": { + Type: schema.TypeString, + Description: "The name of the bucket to put packages.", + Optional: true, + Computed: true, + ForceNew: true, + }, + "container_registry_bucket_name": { + Type: schema.TypeString, + Description: "The name of the bucket to put container registry objects.", + Optional: true, + Computed: true, + ForceNew: true, + }, + "to": { + Type: schema.TypeString, + Description: "Mail address where alerts are sent.", + Optional: true, + }, + "gitlab_url": { + Type: schema.TypeString, + Description: "URL for GitLab.", + Computed: true, + }, + "registry_url": { + Type: schema.TypeString, + Description: "URL for GitLab container registry.", + Computed: true, + }, + "public_ip_address": { + Type: schema.TypeString, + Description: "Public IP address for the DevOps instance.", + Computed: true, + }, + } +} diff --git a/nifcloud/resources/devops/devopsinstance/update.go b/nifcloud/resources/devops/devopsinstance/update.go new file mode 100644 index 0000000..670aa4c --- /dev/null +++ b/nifcloud/resources/devops/devopsinstance/update.go @@ -0,0 +1,76 @@ +package devopsinstance + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nifcloud/terraform-provider-nifcloud/nifcloud/client" +) + +func updateInstance(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + svc := meta.(*client.Client).DevOps + + if d.HasChanges("instance_type", "firewall_group_name", "description") && !d.IsNewResource() { + input := expandUpdateInstanceInput(d) + + _, err := svc.UpdateInstance(ctx, input) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to update a DevOps instance: %s", err)) + } + + if err := waitUntilInstanceRunning(ctx, d, svc); err != nil { + return diag.FromErr(fmt.Errorf("failed to wait for the DevOps instance to become ready: %s", err)) + } + } + + if d.HasChange("disk_size") && !d.IsNewResource() { + o, n := d.GetChange("disk_size") + + // New value has been validated with ValidateFunc and CustomizeDiff already + nTimesToInvoke := (n.(int) - o.(int)) / 100 + + // Make request multiple times to satisfy configured values as a single ExtendDisk invocation extends 100GB of disk + for i := 0; i < nTimesToInvoke; i++ { + input := expandExtendDiskInput(d) + + _, err := svc.ExtendDisk(ctx, input) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to extend a disk of the DevOps instance: %s", err)) + } + + if err := waitUntilInstanceRunning(ctx, d, svc); err != nil { + return diag.FromErr(fmt.Errorf("failed to wait for the DevOps instance to become ready: %s", err)) + } + } + } + + if d.HasChanges("network_id", "private_address") && !d.IsNewResource() { + input := expandUpdateNetworkInterfaceInput(d) + + _, err := svc.UpdateNetworkInterface(ctx, input) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to update a network interface of the DevOps instance: %s", err)) + } + + if err := waitUntilInstanceRunning(ctx, d, svc); err != nil { + return diag.FromErr(fmt.Errorf("failed to wait for the DevOps instance to become ready: %s", err)) + } + } + + if d.HasChange("to") { + input := expandSetupAlertInput(d) + + _, err := svc.SetupAlert(ctx, input) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to setup alert for the DevOps instance: %s", err)) + } + + if err := waitUntilInstanceRunning(ctx, d, svc); err != nil { + return diag.FromErr(fmt.Errorf("failed to wait for the DevOps instance to become ready: %s", err)) + } + } + + return readInstance(ctx, d, meta) +}