From 5e4b4210e5cbba568edeb26d091db2b6c91d76c7 Mon Sep 17 00:00:00 2001 From: youngmn <105404366+youngmn@users.noreply.github.com> Date: Sat, 28 Sep 2024 01:02:54 +0900 Subject: [PATCH] feat(vserver): Add the ability to create a KVM hypervisor type 3Gen server (#455) * feat(vserver): Add VSERVER image numbers Data Source * feat(vserver): Add VSERVER specs Data Source * feat(vserver): Add the ability to create a KVM hypervisor type Gen3 server * fix(vserver): Don't set the state when error responded * review(vserver): Modify based on reviews --- docs/data-sources/server.md | 9 +- docs/data-sources/server_image_numbers.md | 105 +++++ docs/data-sources/server_specs.md | 84 ++++ docs/resources/block_storage.md | 4 +- docs/resources/server.md | 50 +-- examples/server/main.tf | 39 +- go.mod | 2 +- go.sum | 4 +- internal/common/json.go | 10 +- internal/provider/fwprovider/provider.go | 2 + internal/service/server/block_storage.go | 22 +- internal/service/server/server.go | 184 +++++++-- .../service/server/server_data_source_test.go | 11 +- .../server_image_numbers_data_source.go | 359 ++++++++++++++++++ .../server_image_numbers_data_source_test.go | 32 ++ .../server/server_specs_data_source.go | 291 ++++++++++++++ .../server/server_specs_data_source_test.go | 39 ++ internal/service/server/server_test.go | 79 ++++ 18 files changed, 1245 insertions(+), 81 deletions(-) create mode 100644 docs/data-sources/server_image_numbers.md create mode 100644 docs/data-sources/server_specs.md create mode 100644 internal/service/server/server_image_numbers_data_source.go create mode 100644 internal/service/server/server_image_numbers_data_source_test.go create mode 100644 internal/service/server/server_specs_data_source.go create mode 100644 internal/service/server/server_specs_data_source_test.go diff --git a/docs/data-sources/server.md b/docs/data-sources/server.md index d958d10fa..2dacbdc7e 100644 --- a/docs/data-sources/server.md +++ b/docs/data-sources/server.md @@ -11,7 +11,7 @@ This module can be useful for getting detail of Server instance created before. #### Basic usage -```hcl +```terraform variable "instance_no" {} data "ncloud_server" "server" { @@ -21,7 +21,7 @@ data "ncloud_server" "server" { #### Usage of using filter -```hcl +```terraform variable "subnet_no" {} variable "name" {} @@ -84,4 +84,7 @@ The following arguments are supported: * `private_ip` - IP address of the network interface. * `init_script_no` - The ID of Init script. * `placement_group_no` - The ID of Physical placement group. -* `is_encrypted_base_block_storage_volume` - Whether to encrypt basic block storage if server image is RHV. \ No newline at end of file +* `is_encrypted_base_block_storage_volume` - Whether to encrypt basic block storage if server image is RHV. +* `hypervisor_type` - Hypervisor type. (`XEN` or `KVM`) +* `server_image_number` - Server image number. +* `server_spec_code` - Server spec code. diff --git a/docs/data-sources/server_image_numbers.md b/docs/data-sources/server_image_numbers.md new file mode 100644 index 000000000..ab5fa99b4 --- /dev/null +++ b/docs/data-sources/server_image_numbers.md @@ -0,0 +1,105 @@ +--- +subcategory: "Server" +--- + + +# Data Source: ncloud_server_image_numbers + +To create a server instance (VM), you should select a server image. This data source gets a list of server images. You can look up the image numbers of Gen2(XEN) and Gen3(KVM) servers. + +~> **NOTE:** This only supports VPC environment. + +## Example Usage + +The following example shows how to take a list of Server image. + +```terraform +data "ncloud_server_image_numbers" "example" { + output_file = "image.json" +} + +output "image_list" { + value = { + for image in data.ncloud_server_image_numbers.example.image_number_list: + image.server_image_number => [image.name, image.hypervisor_type] + } +} +``` + +Outputs: +```terraform +image_list = { + "16187005" = [ + "ubuntu-20.04", + "XEN", + ] + "16187007" = [ + "win-2019-64-en", + "XEN", + ] + "17552318" = [ + "ubuntu-20.04-base", + "KVM", + ] + "23789321" = [ + "ubuntu-22.04-gpu", + "KVM", + ] + "25495367" = [ + "rocky-8.10-base", + "KVM", + ] + "25623982" = [ + "rocky-8.10-gpu", + "KVM", + ] + "25624115" = [ + "rocky-8.10-base", + "XEN", + ] +} +``` + +```terraform +data "ncloud_server_image_numbers" "example" { + filter { + name = "name" + values = ["rocky-8.10-base"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `output_file` - (Optional) The name of file that can save data source after running `terraform plan`. +* `filter` - (Optional) Custom filter block as described below. + * `name` - (Required) The name of the field to filter by + * `values` - (Required) Set of values that are accepted for the given field. + * `regex` - (Optional) is `values` treated as a regular expression. + +## Attributes Reference + +This data source exports the following attributes in addition to the arguments above: + +* `image_number_list` - List of SEVER image number. + * `server_image_number` - Server image number. + * `name` - Server image name. + * `description` - Server image description. + * `type` - Server image type. (`SELF` or `NCP`) + * `hypervisor_type` - Server image hypervisor type. (`XEN` or `KVM`) + * `cpu_architecture_type` - Server image cpu type. + * `os_category_type` - Server image os category type. (`LINUX` or `WINDOWS`) + * `os_type` - Server image os type. (`ROCKY` or `UBUNTU` or `CENTOS` or `WINDOWS`) + * `product_code` - The code of image product. + * `block_storage_mapping_list` - List of block storage allocated to the server image. Viewable after the server image is created. + * `order` - Block storage order. + * `block_storage_snapshot_instance_no` - Block storage snapshot instance number. + * `block_storage_snapshot_name` - Block storage snapshot name. + * `block_storage_size` - Block storage size (byte). + * `block_storage_name` - Block storage name. + * `block_storage_volume_type` - Block storage volume type. (`SSD` or `HDD` or `CB1` or `FB1`). + * `iops` - IOPS. + * `throughput` - Load balancing performance. + * `is_encrypted_volume` - Volume encryption status. (`true` or `false`) diff --git a/docs/data-sources/server_specs.md b/docs/data-sources/server_specs.md new file mode 100644 index 000000000..375c151ee --- /dev/null +++ b/docs/data-sources/server_specs.md @@ -0,0 +1,84 @@ +--- +subcategory: "Server" +--- + + +# Data Source: ncloud_server_specs + +To create a server instance (VM), you should select a server spec. This data source gets a list of server specs. You can look up the spec code of servers. + +~> **NOTE:** This only supports VPC environment. + +## Example Usage + +The following example shows how to take a list of Server spec. + +```terraform +data "ncloud_server_specs" "example" { + output_file = "spec.json" +} + +output "spec_list" { + value = { + for spec in data.ncloud_server_specs.example.server_spec_list: + spec.server_spec_code => [spec.description, spec.generation_code] + } +} +``` + +Outputs: +```terraform +spec_list = { + "c2-g3" = [ + "vCPU 2EA, Memory 4GB", + "G3", + ] + "m2-g3" = [ + "vCPU 2EA, Memory 16GB", + "G3", + ] + "c2-g2-s50" = [ + "vCPU 2EA, Memory 4GB, [SSD]Disk 50GB", + "G2", + ] +} +``` + +```terraform +data "ncloud_server_specs" "example" { + filter { + name = "server_spec_code" + values = ["c2-g3"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `output_file` - (Optional) The name of file that can save data source after running `terraform plan`. +* `filter` - (Optional) Custom filter block as described below. + * `name` - (Required) The name of the field to filter by + * `values` - (Required) Set of values that are accepted for the given field. + * `regex` - (Optional) is `values` treated as a regular expression. + +## Attributes Reference + +This data source exports the following attributes in addition to the arguments above: + +* `server_spec_list` - List of SEVER spec. + * `server_spec_code` - Server spec code. + * `hypervisor_type` - Server hypervisor type. (`XEN` or `KVM`) + * `generation_code` - Server generation code. (`G2` or `G3`) + * `cpu_architecture_type` - Server cpu type. + * `cpu_count` - Server cpu count. + * `memory_size` - Server memory size(Byte). + * `block_storage_max_count` - Maximum number of BlockStorage that can be allocated. + * `block_storage_max_iops` - BlockStorage max IOPS. + * `block_storage_max_throughput` - BlockStorage max throughput(Mbps). + * `network_performance` - Network performance(bps). + * `network_interface_max_count` - Maximum number of network interfaces that can be allocated. + * `gpu_count` - GPU count. + * `description` - Server sepc description. + * `product_code` - The code of server product. diff --git a/docs/resources/block_storage.md b/docs/resources/block_storage.md index 3b3d3a8df..3c1e969cc 100644 --- a/docs/resources/block_storage.md +++ b/docs/resources/block_storage.md @@ -9,7 +9,7 @@ Provides a Block Storage resource. ## Example Usage -```hcl +```terraform resource "ncloud_block_storage" "storage" { server_instance_no = "812345" name = "tf-test-storage1" @@ -23,7 +23,7 @@ The following arguments are supported: * `size` - (Required) The size of the block storage to create. It is automatically set when you take a snapshot. * `server_instance_no` - **(Required) When first created**. (Optional) After creation. Server instance ID to which you want to assign the block storage. -* `name` - (Optional) The name to create. If omitted, Terraform will assign a random, unique name. +* `name` - (Optional) The name to create. If omitted, Terraform will assign a random, unique name. Min: 3, Max: 30. Only English letters, numbers, and the special character "-" can be used. It must start with an English letter. It must end with an English letter or number. * `description` - (Optional) description to create. * `disk_detail_type` - (Optional) Type of block storage disk detail to create. Default `SSD`. Accepted values: `SSD` | `HDD` * `stop_instance_before_detaching` - (Optional, Boolean) Set this to true to ensure that the target instance is stopped before trying to detach the block storage. It stops the instance, if it is not already stopped. diff --git a/docs/resources/server.md b/docs/resources/server.md index 56d8b3d4d..850d5700a 100644 --- a/docs/resources/server.md +++ b/docs/resources/server.md @@ -11,7 +11,7 @@ Provides a Server instance resource. #### Basic (Classic) -```hcl +```terraform resource "ncloud_server" "server" { name = "tf-test-vm1" server_image_product_code = "SPSW0LINUX000032" @@ -31,7 +31,7 @@ resource "ncloud_server" "server" { #### Basic (VPC) -```hcl +```terraform resource "ncloud_login_key" "loginkey" { key_name = "test-key" } @@ -52,14 +52,15 @@ resource "ncloud_subnet" "test" { resource "ncloud_server" "server" { subnet_no = ncloud_subnet.test.id name = "my-tf-server" - server_image_product_code = "SW.VSVR.OS.LNX64.CNTOS.0703.B050" + server_image_number = "25495367" + server_spec_code = "s2-g3" login_key_name = ncloud_login_key.loginkey.key_name } ``` -#### Create VPC instance reference by data source (retrieve image and product code) +#### Create VPC instance reference by data source (retrieve server_image_number and server_spec_code) -```hcl +```terraform resource "ncloud_login_key" "loginkey" { key_name = "test-key" } @@ -80,34 +81,22 @@ resource "ncloud_subnet" "test" { resource "ncloud_server" "server" { subnet_no = ncloud_subnet.test.id name = "my-tf-server" - server_image_product_code = data.ncloud_server_image.server_image.id - server_product_code = data.ncloud_server_product.product.id + server_image_number = data.ncloud_server_image_numbers.server_image.image_number_list.0.server_image_number + server_spec_code = data.ncloud_server_specs.spec.server_spec_list.0.server_sepc_code login_key_name = ncloud_login_key.loginkey.key_name } -data "ncloud_server_image" "server_image" { +data "ncloud_server_image_numbers" "server_image" { filter { - name = "os_information" - values = ["CentOS 7.3 (64-bit)"] + name = "name" + values = ["ubuntu-20.04-base"] } } -data "ncloud_server_product" "product" { - server_image_product_code = data.ncloud_server_image.server_image.id - - filter { - name = "product_code" - values = ["SSD"] - } - - filter { - name = "cpu_count" - values = ["2"] - } - +data "ncloud_server_specs" "spec" { filter { - name = "memory_size" - values = ["8GB"] + name = "server_spec_code" + values = ["s2-g3"] } } ``` @@ -116,17 +105,15 @@ data "ncloud_server_product" "product" { The following arguments are supported: -* `server_image_product_code` - (Optional, Required if `member_server_image_no` is not provided) Server image product code to determine which server image to create. It can be obtained through `data.ncloud_server_image(s)`. - - [Docs server Image Products](https://github.com/NaverCloudPlatform/terraform-ncloud-docs/blob/main/docs/server_image_product.md) +* `server_image_product_code` - (Optional, Required if `member_server_image_no` or `server_image_number` is not provided) Server image product code to determine which server image to create. It can be obtained through `data.ncloud_server_image(s)`. - [`ncloud_server_image` data source](../data-sources/server_image.md) - [`ncloud_server_images` data source](../data-sources/server_images.md) * `server_product_code` - (Optional) Server product code to determine the server specification to create. It can be obtained through the `data.ncloud_server_product(s)` action. Default : Selected as minimum specification. The minimum standards are 1. memory 2. CPU 3. basic block storage size 4. disk type (NET,LOCAL) - - [Docs server Image Products](https://github.com/NaverCloudPlatform/terraform-ncloud-docs/blob/main/docs/server_image_product.md) - [`ncloud_server_product` data source](../data-sources/server_product.md) - [`ncloud_server_products` data source](../data-sources/server_products.md) -* `member_server_image_no` - (Optional, Required if `server_image_product_code` is not provided) Required value when creating a server from a manually created server image. It can be obtained through the `data.ncloud_member_server_image(s)` action. +* `member_server_image_no` - (Optional, Required if `server_image_product_code` or `server_image_number` is not provided) Required value when creating a server from a manually created server image. It can be obtained through the `data.ncloud_member_server_image(s)` action. - [`ncloud_member_server_image` data source](../data-sources/member_server_image.md) - [`ncloud_member_server_images` data source](../data-sources/member_server_images.md) @@ -149,6 +136,10 @@ The following arguments are supported: ~> **NOTE:** Below arguments only support VPC environment. Please set `support_vpc` of provider to `true` * `subnet_no` - (Required) The ID of the associated Subnet. +* `server_image_number` - (Optional, Required if `server_image_product_code` or `member_server_image_no` is not provided) Required to create a KVM hypervisor type 3rd generation server. Server image number to determine which server image to create. It can be obtained through `data.ncloud_server_image_numbers`. + - [`ncloud_server_image_numbers` data source](../data-sources/server_image_numbers.md) +* `server_spec_code` - (Optional, Required if to select the spec) Available only if `server_image_number` is entered. Server spec code to determine the server specification to create. It can be obtained through the `data.ncloud_server_specs` action. Default : Selected as minimum specification. The minimum standards are 1. memory 2. CPU 3. basic block storage size 4. disk type (NET,LOCAL) + - [`ncloud_server_specs` data source](../data-sources/server_specs.md) * `init_script_no` - (Optional) Set init script ID, The server can run a user-set initialization script at first boot. * `placement_group_no` - (Optional) Physical placement group that belongs to the server instance. * `network_interface` - (Optional) List of Network Interface. You can assign up to three network interfaces. @@ -176,6 +167,7 @@ The following arguments are supported: ~> **NOTE:** Below attributes only provide VPC environment. * `vpc_no` - The ID of the VPC where you want to place the Server Instance. +* `hypervisor_type` - Hypervisor type. (`XEN` or `KVM`) * `network_interface` - List of Network Interface. * `subnet_no` - Subnet ID of the network interface. * `private_ip` - IP address of the network interface. diff --git a/examples/server/main.tf b/examples/server/main.tf index 26ece7e26..2d54b73b7 100644 --- a/examples/server/main.tf +++ b/examples/server/main.tf @@ -7,12 +7,41 @@ provider "ncloud" { data "ncloud_regions" "regions" { } -data "ncloud_server_images" "server_images" { +data "ncloud_server_image_numbers" "server_images" { + filter { + name = "name" + values = ["ubuntu-20.04-base"] + } } -resource "ncloud_server" "server" { - name = var.server_name - server_image_product_code = var.server_image_product_code - server_product_code = var.server_product_code +data "ncloud_server_specs" "spec" { + filter { + name = "server_spec_code" + values = ["c2-g3"] + } +} + +resource "ncloud_login_key" "loginkey" { + key_name = "test-key" +} + +resource "ncloud_vpc" "test" { + ipv4_cidr_block = "10.0.0.0/16" } +resource "ncloud_subnet" "test" { + vpc_no = ncloud_vpc.test.vpc_no + subnet = cidrsubnet(ncloud_vpc.test.ipv4_cidr_block, 8, 1) + zone = "KR-2" + network_acl_no = ncloud_vpc.test.default_network_acl_no + subnet_type = "PUBLIC" + usage_type = "GEN" +} + +resource "ncloud_server" "server" { + subnet_no = ncloud_subnet.test.id + name = var.server_name + server_image_number = data.ncloud_server_image_numbers.server_images.image_number_list.0.server_image_number + server_spec_code = data.ncloud_server_specs.spec.server_spec_list.0.server_spec_code + login_key_name = ncloud_login_key.loginkey.key_name +} diff --git a/go.mod b/go.mod index 33445dbf9..7fb0c302c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/terraform-providers/terraform-provider-ncloud go 1.21 require ( - github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.20 + github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.21 github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/credentials v1.17.27 github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 diff --git a/go.sum b/go.sum index e288a82e4..828314a55 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.20 h1:M6cavZ5dahgfO+ajWf+8ftgCHzE4Vd6Na6ztXIVmpsY= -github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.20/go.mod h1:jRp8KZ64MUevBWNqehghhG2oF5/JU3Dmt/Cu7dp1mQE= +github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.21 h1:1Spthi+w3COs9jDDcB0262p2od0Be1i9rm3UCIrtzoA= +github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.21/go.mod h1:jRp8KZ64MUevBWNqehghhG2oF5/JU3Dmt/Cu7dp1mQE= github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= diff --git a/internal/common/json.go b/internal/common/json.go index 746b01b21..f94b66ef5 100644 --- a/internal/common/json.go +++ b/internal/common/json.go @@ -1,6 +1,9 @@ package common -import "encoding/json" +import ( + "encoding/json" + "regexp" +) // MarshalUnchecked return the JSON encoding of value // It does not check for errors about marshaling failing, so be careful to use @@ -12,3 +15,8 @@ func MarshalUnchecked(value interface{}) []byte { func MarshalUncheckedString(value interface{}) string { return string(MarshalUnchecked(value)) } + +func ReplaceNull(s string) string { + re := regexp.MustCompile(`:`) + return re.ReplaceAllString(s, ":null") +} diff --git a/internal/provider/fwprovider/provider.go b/internal/provider/fwprovider/provider.go index 6d982dbf3..38797f128 100644 --- a/internal/provider/fwprovider/provider.go +++ b/internal/provider/fwprovider/provider.go @@ -82,6 +82,8 @@ func (p *fwprovider) DataSources(ctx context.Context) []func() datasource.DataSo dataSources = append(dataSources, vpc.NewVpcPeeringDataSource) dataSources = append(dataSources, server.NewInitScriptDataSource) dataSources = append(dataSources, server.NewLoginKeyDataSource) + dataSources = append(dataSources, server.NewServerImageNumbersDataSource) + dataSources = append(dataSources, server.NewServerSpecsDataSource) dataSources = append(dataSources, mysql.NewMysqlDataSource) dataSources = append(dataSources, mysql.NewMysqlImageProductsDataSource) dataSources = append(dataSources, mysql.NewMysqlProductsDataSource) diff --git a/internal/service/server/block_storage.go b/internal/service/server/block_storage.go index 6974ccc75..dbbb63162 100644 --- a/internal/service/server/block_storage.go +++ b/internal/service/server/block_storage.go @@ -19,9 +19,13 @@ import ( ) const ( - BlockStorageStatusCodeCreate = "CREAT" - BlockStorageStatusCodeInit = "INIT" - BlockStorageStatusCodeAttach = "ATTAC" + BlockStorageStatusCodeCreate = "CREAT" + BlockStorageStatusCodeInit = "INIT" + BlockStorageStatusCodeAttach = "ATTAC" + BlockStorageStatusNameInit = "initialized" + BlockStorageStatusNameOptimizing = "optimizing" + BlockStorageStatusNameAttach = "attached" + BlockStorageStatusNameDetach = "detached" ) func ResourceNcloudBlockStorage() *schema.Resource { @@ -378,7 +382,7 @@ func getVpcBlockStorage(config *conn.ProviderConfig, id string) (*BlockStorage, if len(resp.BlockStorageInstanceList) > 0 { inst := resp.BlockStorageInstanceList[0] - return &BlockStorage{ + blockStorage := BlockStorage{ BlockStorageInstanceNo: inst.BlockStorageInstanceNo, ServerInstanceNo: inst.ServerInstanceNo, BlockStorageType: inst.BlockStorageType.Code, @@ -387,12 +391,17 @@ func getVpcBlockStorage(config *conn.ProviderConfig, id string) (*BlockStorage, DeviceName: inst.DeviceName, BlockStorageProductCode: inst.BlockStorageProductCode, Status: inst.BlockStorageInstanceStatus.Code, - Operation: inst.BlockStorageInstanceOperation.Code, + StatusName: inst.BlockStorageInstanceStatusName, Description: inst.BlockStorageDescription, DiskType: inst.BlockStorageDiskType.Code, DiskDetailType: inst.BlockStorageDiskDetailType.Code, ZoneCode: inst.ZoneCode, - }, nil + } + if inst.BlockStorageInstanceOperation != nil { + blockStorage.Operation = inst.BlockStorageInstanceOperation.Code + } + + return &blockStorage, nil } return nil, nil @@ -722,6 +731,7 @@ type BlockStorage struct { BlockStorageProductCode *string `json:"product_code,omitempty"` Status *string `json:"status,omitempty"` Operation *string `json:"operation,omitempty"` + StatusName *string `json:"status_name,omitempty"` Description *string `json:"description,omitempty"` DiskType *string `json:"disk_type,omitempty"` DiskDetailType *string `json:"disk_detail_type,omitempty"` diff --git a/internal/service/server/server.go b/internal/service/server/server.go index 290be11f9..f0c225c10 100644 --- a/internal/service/server/server.go +++ b/internal/service/server/server.go @@ -12,6 +12,7 @@ import ( "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/server" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -43,17 +44,29 @@ func ResourceNcloudServer() *schema.Resource { ForceNew: true, ConflictsWith: []string{"member_server_image_no"}, }, - "server_product_code": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, "member_server_image_no": { Type: schema.TypeString, Optional: true, ForceNew: true, ConflictsWith: []string{"server_image_product_code"}, }, + "server_image_number": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"server_image_product_code"}, + }, + "server_product_code": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "server_spec_code": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, "name": { Type: schema.TypeString, Optional: true, @@ -187,7 +200,6 @@ func ResourceNcloudServer() *schema.Resource { Computed: true, ForceNew: true, }, - "instance_no": { Type: schema.TypeString, Computed: true, @@ -212,6 +224,10 @@ func ResourceNcloudServer() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "hypervisor_type": { + Type: schema.TypeString, + Computed: true, + }, "public_ip": { Type: schema.TypeString, Computed: true, @@ -322,7 +338,7 @@ func resourceNcloudServerDelete(d *schema.ResourceData, meta interface{}) error return err } - if err := waitForDisconnectBlockStorage(config, d, blockStorage); err != nil { + if err := waitForDisconnectBlockStorage(config, *blockStorage.BlockStorageInstanceNo); err != nil { return err } } @@ -342,7 +358,7 @@ func resourceNcloudServerDelete(d *schema.ResourceData, meta interface{}) error func resourceNcloudServerUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*conn.ProviderConfig) - if d.HasChange("server_product_code") { + if d.HasChange("server_product_code") || d.HasChange("server_spec_code") { if err := updateServerInstanceSpec(d, config); err != nil { return err } @@ -451,6 +467,8 @@ func createVpcServerInstance(d *schema.ResourceData, config *conn.ProviderConfig ServerProductCode: StringPtrOrNil(d.GetOk("server_product_code")), ServerImageProductCode: StringPtrOrNil(d.GetOk("server_image_product_code")), MemberServerImageInstanceNo: StringPtrOrNil(d.GetOk("member_server_image_no")), + ServerImageNo: StringPtrOrNil(d.GetOk("server_image_number")), + ServerSpecCode: StringPtrOrNil(d.GetOk("server_spec_code")), ServerName: StringPtrOrNil(d.GetOk("name")), ServerDescription: StringPtrOrNil(d.GetOk("description")), LoginKeyName: StringPtrOrNil(d.GetOk("login_key_name")), @@ -513,11 +531,24 @@ func createVpcServerInstance(d *schema.ResourceData, config *conn.ProviderConfig return nil, err } + blockStorageList, err := getVpcBasicBlockStorageList(config, *serverInstance.ServerInstanceNo) + if err != nil { + return nil, err + } + + if len(blockStorageList) > 0 { + for _, blockStorage := range blockStorageList { + if err := waitForAttachedBlockStorage(config, *blockStorage.BlockStorageInstanceNo); err != nil { + return nil, err + } + } + } + return serverInstance.ServerInstanceNo, nil } func waitStateNcloudServerForCreation(config *conn.ProviderConfig, id string) error { - stateConf := &resource.StateChangeConf{ + stateConf := &retry.StateChangeConf{ Pending: []string{"INIT", "CREAT"}, Target: []string{"RUN"}, Refresh: func() (interface{}, string, error) { @@ -577,7 +608,7 @@ func changeServerInstanceSpec(d *schema.ResourceData, config *conn.ProviderConfi return err } - stateConf := &resource.StateChangeConf{ + stateConf := &retry.StateChangeConf{ Pending: []string{"CHNG"}, Target: []string{"NULL"}, Refresh: func() (interface{}, string, error) { @@ -621,9 +652,15 @@ func changeClassicServerInstanceSpec(d *schema.ResourceData, config *conn.Provid func changeVpcServerInstanceSpec(d *schema.ResourceData, config *conn.ProviderConfig) error { reqParams := &vserver.ChangeServerInstanceSpecRequest{ - RegionCode: &config.RegionCode, - ServerInstanceNo: ncloud.String(d.Get("instance_no").(string)), - ServerProductCode: ncloud.String(d.Get("server_product_code").(string)), + RegionCode: &config.RegionCode, + ServerInstanceNo: ncloud.String(d.Get("instance_no").(string)), + } + + if d.HasChange("server_product_code") { + reqParams.ServerProductCode = ncloud.String(d.Get("server_product_code").(string)) + } + if d.HasChange("server_spec_code") { + reqParams.ServerSpecCode = ncloud.String(d.Get("server_spec_code").(string)) } LogCommonRequest("changeVpcServerInstanceSpec", reqParams) @@ -692,7 +729,7 @@ func startThenWaitServerInstance(config *conn.ProviderConfig, id string) error { return err } - stateConf := &resource.StateChangeConf{ + stateConf := &retry.StateChangeConf{ Pending: []string{"NSTOP"}, Target: []string{"RUN"}, Refresh: func() (interface{}, string, error) { @@ -849,7 +886,9 @@ func convertVcpServerInstance(r *vserver.ServerInstance) *ServerInstance { instance := &ServerInstance{ ServerImageProductCode: r.ServerImageProductCode, + ServerImageNo: r.ServerImageNo, ServerProductCode: r.ServerProductCode, + ServerSpecCode: r.ServerSpecCode, ServerName: r.ServerName, ServerDescription: r.ServerDescription, LoginKeyName: r.LoginKeyName, @@ -868,6 +907,7 @@ func convertVcpServerInstance(r *vserver.ServerInstance) *ServerInstance { SubnetNo: r.SubnetNo, InitScriptNo: r.InitScriptNo, PlacementGroupNo: r.PlacementGroupNo, + HypervisorType: r.HypervisorType.Code, } for _, networkInterfaceNo := range r.NetworkInterfaceNoList { @@ -912,7 +952,7 @@ func buildNetworkInterfaceList(config *conn.ProviderConfig, r *ServerInstance) e func stopThenWaitServerInstance(config *conn.ProviderConfig, id string) error { var err error - stateConf := &resource.StateChangeConf{ + stateConf := &retry.StateChangeConf{ Pending: []string{"SETUP"}, Target: []string{"NULL"}, Refresh: func() (interface{}, string, error) { @@ -943,7 +983,7 @@ func stopThenWaitServerInstance(config *conn.ProviderConfig, id string) error { return err } - stateConf = &resource.StateChangeConf{ + stateConf = &retry.StateChangeConf{ Pending: []string{"RUN"}, Target: []string{"NSTOP"}, Refresh: func() (interface{}, string, error) { @@ -968,7 +1008,7 @@ func stopThenWaitServerInstance(config *conn.ProviderConfig, id string) error { } func detachThenWaitServerInstance(config *conn.ProviderConfig, id string) error { - stateConf := &resource.StateChangeConf{ + stateConf := &retry.StateChangeConf{ Pending: []string{"SETUP"}, Target: []string{"NULL"}, Refresh: func() (interface{}, string, error) { @@ -1035,7 +1075,7 @@ func terminateThenWaitServerInstance(config *conn.ProviderConfig, id string) err return err } - stateConf := &resource.StateChangeConf{ + stateConf := &retry.StateChangeConf{ Pending: []string{"NSTOP"}, Target: []string{"TERMINATED"}, Refresh: func() (interface{}, string, error) { @@ -1143,6 +1183,30 @@ func getVpcAdditionalBlockStorageList(config *conn.ProviderConfig, id string) ([ return blockStorageList, nil } +func getVpcBasicBlockStorageList(config *conn.ProviderConfig, id string) ([]*BlockStorage, error) { + resp, err := config.Client.Vserver.V2Api.GetBlockStorageInstanceList(&vserver.GetBlockStorageInstanceListRequest{ + RegionCode: &config.RegionCode, + ServerInstanceNo: ncloud.String(id), + }) + + if err != nil { + return nil, err + } + + LogResponse("getVpcBasicBlockStorageList", resp) + + if len(resp.BlockStorageInstanceList) < 1 { + return nil, nil + } + + blockStorageList := make([]*BlockStorage, 0) + for _, blockStorage := range resp.BlockStorageInstanceList { + blockStorageList = append(blockStorageList, convertVpcBlockStorage(blockStorage)) + } + + return blockStorageList, nil +} + func getClassicAdditionalBlockStorageList(config *conn.ProviderConfig, id string) ([]*BlockStorage, error) { resp, err := config.Client.Server.V2Api.GetBlockStorageInstanceList(&server.GetBlockStorageInstanceListRequest{ RegionNo: &config.RegionCode, @@ -1178,7 +1242,7 @@ func convertVpcBlockStorage(storage *vserver.BlockStorageInstance) *BlockStorage DeviceName: storage.DeviceName, BlockStorageProductCode: storage.BlockStorageProductCode, Status: storage.BlockStorageInstanceStatus.Code, - Operation: storage.BlockStorageInstanceOperation.Code, + StatusName: storage.BlockStorageInstanceStatusName, Description: storage.BlockStorageDescription, DiskType: storage.BlockStorageDiskType.Code, DiskDetailType: storage.BlockStorageDiskDetailType.Code, @@ -1238,17 +1302,74 @@ func disconnectClassicBlockStorage(config *conn.ProviderConfig, storage *BlockSt return nil } -func waitForDisconnectBlockStorage(config *conn.ProviderConfig, d *schema.ResourceData, storage *BlockStorage) error { - return resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { - blockStorage, err := GetBlockStorage(config, *storage.BlockStorageInstanceNo) - if err != nil { - return resource.RetryableError(err) - } - if *blockStorage.Status != BlockStorageStatusCodeCreate { - return resource.RetryableError(fmt.Errorf("sill connected block storage(%s)", *blockStorage.BlockStorageInstanceNo)) - } - return nil - }) +func waitForDisconnectBlockStorage(config *conn.ProviderConfig, no string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{BlockStorageStatusNameAttach}, + Target: []string{BlockStorageStatusNameDetach}, + Refresh: func() (interface{}, string, error) { + resp, err := GetBlockStorage(config, no) + if err != nil { + return 0, "", err + } + + if resp == nil { + return 0, "", fmt.Errorf("GetBlockStorage is nil") + } + + if *resp.StatusName == BlockStorageStatusNameAttach { + return resp, BlockStorageStatusNameAttach, nil + } else if *resp.StatusName == BlockStorageStatusNameDetach { + return resp, BlockStorageStatusNameDetach, nil + } + + return 0, "", fmt.Errorf("error occurred while waiting to detached") + }, + Timeout: 6 * conn.DefaultTimeout, + Delay: 2 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for BlockStorage (%s) to become available: %s", no, err) + } + + return nil +} + +func waitForAttachedBlockStorage(config *conn.ProviderConfig, no string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{BlockStorageStatusNameInit, BlockStorageStatusNameOptimizing}, + Target: []string{BlockStorageStatusNameAttach}, + Refresh: func() (interface{}, string, error) { + resp, err := GetBlockStorage(config, no) + if err != nil { + return 0, "", err + } + + if resp == nil { + return 0, "", fmt.Errorf("GetBlockStorage is nil") + } + + if *resp.StatusName == BlockStorageStatusNameInit { + return resp, BlockStorageStatusNameInit, nil + } else if *resp.StatusName == BlockStorageStatusNameOptimizing { + return resp, BlockStorageStatusNameOptimizing, nil + } else if *resp.StatusName == BlockStorageStatusNameAttach { + return resp, BlockStorageStatusNameAttach, nil + } + + return 0, "", fmt.Errorf("error occurred while waiting to attached") + }, + Timeout: 6 * conn.DefaultTimeout, + Delay: 2 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for BlockStorage (%s) to become available: %s", no, err) + } + + return nil } func getServerZoneNo(config *conn.ProviderConfig, serverInstanceNo string) (string, error) { @@ -1292,6 +1413,9 @@ type ServerInstance struct { BaseBlockStorageDiskDetailType *string `json:"base_block_storage_disk_detail_type,omitempty"` InstanceTagList []*server.InstanceTag `json:"tag_list,omitempty"` // VPC + ServerImageNo *string `json:"server_image_number,omitempty"` + ServerSpecCode *string `json:"server_spec_code,omitempty"` + HypervisorType *string `json:"hypervisor_type,omitempty"` VpcNo *string `json:"vpc_no,omitempty"` SubnetNo *string `json:"subnet_no,omitempty"` InitScriptNo *string `json:"init_script_no,omitempty"` diff --git a/internal/service/server/server_data_source_test.go b/internal/service/server/server_data_source_test.go index 4cf970fd7..c7bc22009 100644 --- a/internal/service/server/server_data_source_test.go +++ b/internal/service/server/server_data_source_test.go @@ -107,11 +107,18 @@ resource "ncloud_subnet" "test" { usage_type = "GEN" } +data "ncloud_server_image_numbers" "server_images" { + filter { + name = "name" + values = ["ubuntu-22.04-base"] + } +} + resource "ncloud_server" "server" { subnet_no = ncloud_subnet.test.id name = "%[1]s" - server_image_product_code = "SW.VSVR.OS.LNX64.CNTOS.0703.B050" - server_product_code = "SVR.VSVR.STAND.C002.M008.NET.HDD.B050.G002" + server_image_number = data.ncloud_server_image_numbers.server_images.image_number_list.0.server_image_number + server_spec_code = "s2-g3" login_key_name = ncloud_login_key.loginkey.key_name } diff --git a/internal/service/server/server_image_numbers_data_source.go b/internal/service/server/server_image_numbers_data_source.go new file mode 100644 index 000000000..af486d22f --- /dev/null +++ b/internal/service/server/server_image_numbers_data_source.go @@ -0,0 +1,359 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vserver" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/terraform-providers/terraform-provider-ncloud/internal/common" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" +) + +var ( + _ datasource.DataSource = &serverImageNumbersDataSource{} + _ datasource.DataSourceWithConfigure = &serverImageNumbersDataSource{} +) + +func NewServerImageNumbersDataSource() datasource.DataSource { + return &serverImageNumbersDataSource{} +} + +type serverImageNumbersDataSource struct { + config *conn.ProviderConfig +} + +func (d *serverImageNumbersDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_server_image_numbers" +} + +func (d *serverImageNumbersDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "output_file": schema.StringAttribute{ + Optional: true, + }, + "image_number_list": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "server_image_number": schema.StringAttribute{ + Computed: true, + }, + "name": schema.StringAttribute{ + Computed: true, + }, + "description": schema.StringAttribute{ + Computed: true, + }, + "type": schema.StringAttribute{ + Computed: true, + }, + "hypervisor_type": schema.StringAttribute{ + Computed: true, + }, + "cpu_architecture_type": schema.StringAttribute{ + Computed: true, + }, + "os_category_type": schema.StringAttribute{ + Computed: true, + }, + "os_type": schema.StringAttribute{ + Computed: true, + }, + "product_code": schema.StringAttribute{ + Computed: true, + }, + "block_storage_mapping_list": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "order": schema.Int32Attribute{ + Computed: true, + }, + "block_storage_snapshot_instance_no": schema.Int32Attribute{ + Computed: true, + }, + "block_storage_snapshot_name": schema.StringAttribute{ + Computed: true, + }, + "block_storage_size": schema.Int64Attribute{ + Computed: true, + }, + "block_storage_name": schema.StringAttribute{ + Computed: true, + }, + "block_storage_volume_type": schema.StringAttribute{ + Computed: true, + }, + "iops": schema.Int32Attribute{ + Computed: true, + }, + "throughput": schema.Int64Attribute{ + Computed: true, + }, + "is_encrypted_volume": schema.BoolAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + }, + }, + }, + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + "filter": common.DataSourceFiltersBlock(), + }, + } +} + +func (d *serverImageNumbersDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.config = config +} + +func (d *serverImageNumbersDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data serverImageNumbersDataSourceModel + + if !d.config.SupportVPC { + resp.Diagnostics.AddError( + "NOT SUPPORT CLASSIC", + "does not support CLASSIC. only VPC.", + ) + return + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + reqParams := &vserver.GetServerImageListRequest{ + RegionCode: &d.config.RegionCode, + } + tflog.Info(ctx, "GetServerImageListRequest reqParams="+common.MarshalUncheckedString(reqParams)) + + imageNoResp, err := d.config.Client.Vserver.V2Api.GetServerImageList(reqParams) + if err != nil { + resp.Diagnostics.AddError("READING ERROR", err.Error()) + return + } + tflog.Info(ctx, "GetServerImageListRequest response="+common.MarshalUncheckedString(imageNoResp)) + + if imageNoResp == nil || len(imageNoResp.ServerImageList) < 1 { + resp.Diagnostics.AddError("READING ERROR", "no result.") + return + } + + imagesNoList, diags := flattenServerImageList(ctx, imageNoResp.ServerImageList) + if diags.HasError() { + resp.Diagnostics.AddError("READING ERROR", "refreshFromOutput error") + return + } + fillteredList := common.FilterModels(ctx, data.Filters, imagesNoList) + diags = data.refreshFromOutput(ctx, fillteredList) + if diags.HasError() { + resp.Diagnostics.AddError("READING ERROR", "refreshFromOutput error") + return + } + + if !data.OutputFile.IsNull() && data.OutputFile.String() != "" { + outputPath := data.OutputFile.ValueString() + + if convertedList, err := convertImagesToJsonStruct(data.ImageNumberList.Elements()); err != nil { + resp.Diagnostics.AddError("OUTPUT FILE ERROR", err.Error()) + return + } else if err := common.WriteToFile(outputPath, convertedList); err != nil { + resp.Diagnostics.AddError("OUTPUT FILE ERROR", err.Error()) + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func convertImagesToJsonStruct(images []attr.Value) ([]serverImageNoToJsonConvert, error) { + var serverImagesToConvert = []serverImageNoToJsonConvert{} + + for _, image := range images { + imageJasn := serverImageNoToJsonConvert{} + if err := json.Unmarshal([]byte(common.ReplaceNull(image.String())), &imageJasn); err != nil { + return nil, err + } + serverImagesToConvert = append(serverImagesToConvert, imageJasn) + } + + return serverImagesToConvert, nil +} + +func flattenServerImageList(ctx context.Context, list []*vserver.ServerImage) ([]*serverImageNo, diag.Diagnostics) { + var outputs []*serverImageNo + var diags diag.Diagnostics + + for _, v := range list { + var output serverImageNo + diags = output.refreshFromOutput(ctx, v) + if diags.HasError() { + return nil, diags + } + + outputs = append(outputs, &output) + } + return outputs, diags +} + +type serverImageNumbersDataSourceModel struct { + ID types.String `tfsdk:"id"` + ImageNumberList types.List `tfsdk:"image_number_list"` + OutputFile types.String `tfsdk:"output_file"` + Filters types.Set `tfsdk:"filter"` +} + +type serverImageNo struct { + Number types.String `tfsdk:"server_image_number"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Type types.String `tfsdk:"type"` + HypervisorType types.String `tfsdk:"hypervisor_type"` + CpuArchitectureType types.String `tfsdk:"cpu_architecture_type"` + OsCategoryType types.String `tfsdk:"os_category_type"` + OsType types.String `tfsdk:"os_type"` + ProductCode types.String `tfsdk:"product_code"` + BlockStorageMapList types.List `tfsdk:"block_storage_mapping_list"` +} + +type blockStorageMap struct { + Order types.Int32 `tfsdk:"order"` + BlockStorageSnapshotInstanceNo types.Int32 `tfsdk:"block_storage_snapshot_instance_no"` + BlockStorageSnapshotName types.String `tfsdk:"block_storage_snapshot_name"` + BlockStorageSize types.Int64 `tfsdk:"block_storage_size"` + BlockStorageName types.String `tfsdk:"block_storage_name"` + BlockStorageVolumeType types.String `tfsdk:"block_storage_volume_type"` + Iops types.Int32 `tfsdk:"iops"` + Throughput types.Int64 `tfsdk:"throughput"` + IsEncryptedVolume types.Bool `tfsdk:"is_encrypted_volume"` +} + +type serverImageNoToJsonConvert struct { + Number string `json:"server_image_number"` + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + HypervisorType string `json:"hypervisor_type"` + CpuArchitectureType string `json:"cpu_architecture_type"` + OsCategoryType string `json:"os_category_type"` + OsType string `json:"os_type"` + ProductCode string `json:"product_code"` + BlockStorageMapList []blockStorageMapToJsonConvert `json:"block_storage_mapping_list"` +} + +type blockStorageMapToJsonConvert struct { + Order int32 `json:"order"` + BlockStorageSnapshotInstanceNo int32 `json:"block_storage_snapshot_instance_no,omitempty"` + BlockStorageSnapshotName string `json:"block_storage_snapshot_name,omitempty"` + BlockStorageSize int64 `json:"block_storage_size"` + BlockStorageName string `json:"block_storage_name,omitempty"` + BlockStorageVolumeType string `json:"block_storage_volume_type"` + Iops int32 `json:"iops,omitempty"` + Throughput int64 `json:"throughput,omitempty"` + IsEncryptedVolume bool `json:"is_encrypted_volume"` +} + +func (d serverImageNo) attrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "server_image_number": types.StringType, + "name": types.StringType, + "description": types.StringType, + "type": types.StringType, + "hypervisor_type": types.StringType, + "cpu_architecture_type": types.StringType, + "os_category_type": types.StringType, + "os_type": types.StringType, + "product_code": types.StringType, + "block_storage_mapping_list": types.ListType{ElemType: types.ObjectType{AttrTypes: blockStorageMap{}.attrTypes()}}, + } +} + +func (d blockStorageMap) attrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "order": types.Int32Type, + "block_storage_snapshot_instance_no": types.Int32Type, + "block_storage_snapshot_name": types.StringType, + "block_storage_size": types.Int64Type, + "block_storage_name": types.StringType, + "block_storage_volume_type": types.StringType, + "iops": types.Int32Type, + "throughput": types.Int64Type, + "is_encrypted_volume": types.BoolType, + } +} + +func (d *serverImageNumbersDataSourceModel) refreshFromOutput(ctx context.Context, list []*serverImageNo) diag.Diagnostics { + imageNoListValue, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: serverImageNo{}.attrTypes()}, list) + if diags.HasError() { + return diags + } + + d.ImageNumberList = imageNoListValue + d.ID = types.StringValue("") + + return diags +} + +func (d *serverImageNo) refreshFromOutput(ctx context.Context, output *vserver.ServerImage) diag.Diagnostics { + d.Number = types.StringPointerValue(output.ServerImageNo) + d.Name = types.StringPointerValue(output.ServerImageName) + d.Description = types.StringPointerValue(output.ServerImageDescription) + d.Type = types.StringPointerValue(output.ServerImageType.Code) + d.HypervisorType = types.StringPointerValue(output.HypervisorType.Code) + d.CpuArchitectureType = types.StringPointerValue(output.CpuArchitectureType.Code) + d.OsCategoryType = types.StringPointerValue(output.OsCategoryType.Code) + d.OsType = types.StringPointerValue(output.OsType.Code) + d.ProductCode = types.StringPointerValue(output.ServerImageProductCode) + + var blockStorageList []blockStorageMap + for _, block := range output.BlockStorageMappingList { + blockStorage := blockStorageMap{ + Order: types.Int32PointerValue(block.Order), + BlockStorageSnapshotInstanceNo: types.Int32PointerValue(block.BlockStorageSnapshotInstanceNo), + BlockStorageSnapshotName: types.StringPointerValue(block.BlockStorageSnapshotName), + BlockStorageSize: types.Int64PointerValue(block.BlockStorageSize), + BlockStorageName: types.StringPointerValue(block.BlockStorageName), + BlockStorageVolumeType: types.StringPointerValue(block.BlockStorageVolumeType.Code), + Iops: types.Int32PointerValue(block.Iops), + Throughput: types.Int64PointerValue(block.Throughput), + IsEncryptedVolume: types.BoolPointerValue(block.IsEncryptedVolume), + } + blockStorageList = append(blockStorageList, blockStorage) + } + blockStorageMaps, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: blockStorageMap{}.attrTypes()}, blockStorageList) + if diags.HasError() { + return diags + } + d.BlockStorageMapList = blockStorageMaps + + return diags +} diff --git a/internal/service/server/server_image_numbers_data_source_test.go b/internal/service/server/server_image_numbers_data_source_test.go new file mode 100644 index 000000000..6f70122d9 --- /dev/null +++ b/internal/service/server/server_image_numbers_data_source_test.go @@ -0,0 +1,32 @@ +package server_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" +) + +func TestAccDataSourceNcloudServerImageNumbers_basic(t *testing.T) { + dataName := "data.ncloud_server_image_numbers.images" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceServerImageNumbersConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(dataName, "image_number_list.0.server_image_number", regexp.MustCompile(`^\d+$`)), + resource.TestMatchResourceAttr(dataName, "image_number_list.1.server_image_number", regexp.MustCompile(`^\d+$`)), + resource.TestMatchResourceAttr(dataName, "image_number_list.2.server_image_number", regexp.MustCompile(`^\d+$`)), + ), + }, + }, + }) +} + +var testAccDataSourceServerImageNumbersConfig = ` +data "ncloud_server_image_numbers" "images" { } +` diff --git a/internal/service/server/server_specs_data_source.go b/internal/service/server/server_specs_data_source.go new file mode 100644 index 000000000..eee3571fa --- /dev/null +++ b/internal/service/server/server_specs_data_source.go @@ -0,0 +1,291 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vserver" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/terraform-providers/terraform-provider-ncloud/internal/common" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" +) + +var ( + _ datasource.DataSource = &serverSpecsDataSource{} + _ datasource.DataSourceWithConfigure = &serverSpecsDataSource{} +) + +func NewServerSpecsDataSource() datasource.DataSource { + return &serverSpecsDataSource{} +} + +type serverSpecsDataSource struct { + config *conn.ProviderConfig +} + +func (d *serverSpecsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_server_specs" +} + +func (d *serverSpecsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "output_file": schema.StringAttribute{ + Optional: true, + }, + "server_spec_list": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "server_spec_code": schema.StringAttribute{ + Computed: true, + }, + "hypervisor_type": schema.StringAttribute{ + Computed: true, + }, + "generation_code": schema.StringAttribute{ + Computed: true, + }, + "cpu_architecture_type": schema.StringAttribute{ + Computed: true, + }, + "cpu_count": schema.Int32Attribute{ + Computed: true, + }, + "memory_size": schema.Int64Attribute{ + Computed: true, + }, + "block_storage_max_count": schema.Int32Attribute{ + Computed: true, + }, + "block_storage_max_iops": schema.Int32Attribute{ + Computed: true, + }, + "block_storage_max_throughput": schema.Int32Attribute{ + Computed: true, + }, + "network_performance": schema.Int64Attribute{ + Computed: true, + }, + "network_interface_max_count": schema.Int32Attribute{ + Computed: true, + }, + "gpu_count": schema.Int32Attribute{ + Computed: true, + }, + "description": schema.StringAttribute{ + Computed: true, + }, + "product_code": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + "filter": common.DataSourceFiltersBlock(), + }, + } +} + +func (d *serverSpecsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.config = config +} + +func (d *serverSpecsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data serverSpecsDataSourceModel + + if !d.config.SupportVPC { + resp.Diagnostics.AddError( + "NOT SUPPORT CLASSIC", + "does not support CLASSIC. only VPC.", + ) + return + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + reqParams := &vserver.GetServerSpecListRequest{ + RegionCode: &d.config.RegionCode, + } + tflog.Info(ctx, "GetServerSpecListRequest reqParams="+common.MarshalUncheckedString(reqParams)) + + specResp, err := d.config.Client.Vserver.V2Api.GetServerSpecList(reqParams) + if err != nil { + resp.Diagnostics.AddError("READING ERROR", err.Error()) + return + } + tflog.Info(ctx, "GetServerSpecListRequest response="+common.MarshalUncheckedString(specResp)) + + if specResp == nil || len(specResp.ServerSpecList) < 1 { + resp.Diagnostics.AddError("READING ERROR", "no result.") + return + } + + specList := flattenServerSpecList(specResp.ServerSpecList) + fillteredList := common.FilterModels(ctx, data.Filters, specList) + diags := data.refreshFromOutput(ctx, fillteredList) + if diags.HasError() { + resp.Diagnostics.AddError("READING ERROR", "refreshFromOutput error") + return + } + + if !data.OutputFile.IsNull() && data.OutputFile.String() != "" { + outputPath := data.OutputFile.ValueString() + + if convertedList, err := convertSpecToJsonStruct(data.ServerSpecList.Elements()); err != nil { + resp.Diagnostics.AddError("OUTPUT FILE ERROR", err.Error()) + return + } else if err := common.WriteToFile(outputPath, convertedList); err != nil { + resp.Diagnostics.AddError("OUTPUT FILE ERROR", err.Error()) + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func convertSpecToJsonStruct(specs []attr.Value) ([]serverSpecToJsonConvert, error) { + var serverSpecToConvert = []serverSpecToJsonConvert{} + + for _, spec := range specs { + specJson := serverSpecToJsonConvert{} + if err := json.Unmarshal([]byte(common.ReplaceNull(spec.String())), &specJson); err != nil { + return nil, err + } + serverSpecToConvert = append(serverSpecToConvert, specJson) + } + + return serverSpecToConvert, nil +} + +func flattenServerSpecList(list []*vserver.ServerSpec) []*serverSpec { + var outputs []*serverSpec + + for _, v := range list { + var output serverSpec + output.refreshFromOutput(v) + + outputs = append(outputs, &output) + } + return outputs +} + +type serverSpecsDataSourceModel struct { + ID types.String `tfsdk:"id"` + ServerSpecList types.List `tfsdk:"server_spec_list"` + OutputFile types.String `tfsdk:"output_file"` + Filters types.Set `tfsdk:"filter"` +} + +type serverSpec struct { + ServerSpecCode types.String `tfsdk:"server_spec_code"` + HypervisorType types.String `tfsdk:"hypervisor_type"` + GenerationCode types.String `tfsdk:"generation_code"` + CpuArchitectureType types.String `tfsdk:"cpu_architecture_type"` + CpuCount types.Int32 `tfsdk:"cpu_count"` + MemorySize types.Int64 `tfsdk:"memory_size"` + BlockStorageMaxCount types.Int32 `tfsdk:"block_storage_max_count"` + BlockStorageMaxIops types.Int32 `tfsdk:"block_storage_max_iops"` + BlockStorageMaxThroughput types.Int32 `tfsdk:"block_storage_max_throughput"` + NetworkPerformance types.Int64 `tfsdk:"network_performance"` + NetworkInterfaceMaxCount types.Int32 `tfsdk:"network_interface_max_count"` + GpuCount types.Int32 `tfsdk:"gpu_count"` + Description types.String `tfsdk:"description"` + ProductCode types.String `tfsdk:"product_code"` +} + +type serverSpecToJsonConvert struct { + ServerSpecCode string `json:"server_spec_code"` + HypervisorType string `json:"hypervisor_type"` + GenerationCode string `json:"generation_code"` + CpuArchitectureType string `json:"cpu_architecture_type"` + CpuCount int `json:"cpu_count,omitempty"` + MemorySize int64 `json:"memory_size,omitempty"` + BlockStorageMaxCount int `json:"block_storage_max_count,omitempty"` + BlockStorageMaxIops int `json:"block_storage_max_iops,omitempty"` + BlockStorageMaxThroughput int `json:"block_storage_max_throughput,omitempty"` + NetworkPerformance int64 `json:"network_performance,omitempty"` + NetworkInterfaceMaxCount int `json:"network_interface_max_count,omitempty"` + GpuCount int `json:"gpu_count,omitempty"` + Description string `json:"description"` + ProductCode string `json:"product_code"` +} + +func (d serverSpec) attrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "server_spec_code": types.StringType, + "hypervisor_type": types.StringType, + "generation_code": types.StringType, + "cpu_architecture_type": types.StringType, + "cpu_count": types.Int32Type, + "memory_size": types.Int64Type, + "block_storage_max_count": types.Int32Type, + "block_storage_max_iops": types.Int32Type, + "block_storage_max_throughput": types.Int32Type, + "network_performance": types.Int64Type, + "network_interface_max_count": types.Int32Type, + "gpu_count": types.Int32Type, + "description": types.StringType, + "product_code": types.StringType, + } +} + +func (d *serverSpecsDataSourceModel) refreshFromOutput(ctx context.Context, list []*serverSpec) diag.Diagnostics { + specListValue, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: serverSpec{}.attrTypes()}, list) + if diags.HasError() { + return diags + } + + d.ServerSpecList = specListValue + d.ID = types.StringValue("") + + return diags +} + +func (d *serverSpec) refreshFromOutput(output *vserver.ServerSpec) { + d.ServerSpecCode = types.StringPointerValue(output.ServerSpecCode) + d.GenerationCode = types.StringPointerValue(output.GenerationCode) + d.CpuArchitectureType = types.StringPointerValue(output.CpuArchitectureType.Code) + d.CpuCount = types.Int32PointerValue(output.CpuCount) + d.MemorySize = types.Int64PointerValue(output.MemorySize) + d.BlockStorageMaxCount = types.Int32PointerValue(output.BlockStorageMaxCount) + d.BlockStorageMaxIops = types.Int32PointerValue(output.BlockStorageMaxIops) + d.BlockStorageMaxThroughput = types.Int32PointerValue(output.BlockStorageMaxThroughput) + d.NetworkPerformance = types.Int64PointerValue(output.NetworkPerformance) + d.NetworkInterfaceMaxCount = types.Int32PointerValue(output.NetworkInterfaceMaxCount) + d.GpuCount = types.Int32PointerValue(output.GpuCount) + d.Description = types.StringPointerValue(output.ServerSpecDescription) + d.ProductCode = types.StringPointerValue(output.ServerProductCode) + + if output.HypervisorType != nil { + d.HypervisorType = types.StringPointerValue(output.HypervisorType.Code) + } +} diff --git a/internal/service/server/server_specs_data_source_test.go b/internal/service/server/server_specs_data_source_test.go new file mode 100644 index 000000000..20cbd1ae9 --- /dev/null +++ b/internal/service/server/server_specs_data_source_test.go @@ -0,0 +1,39 @@ +package server_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" +) + +func TestAccDataSourceNcloudServerSpecs_basic(t *testing.T) { + dataName := "data.ncloud_server_specs.specs" + generationCode := "G3" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceServerSpecsConfig(generationCode), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataName, "server_spec_list.0.generation_code", "G3"), + resource.TestCheckResourceAttr(dataName, "server_spec_list.0.hypervisor_type", "KVM"), + ), + }, + }, + }) +} + +func testAccDataSourceServerSpecsConfig(generationCode string) string { + return fmt.Sprintf(` +data "ncloud_server_specs" "specs" { + filter { + name = "generation_code" + values = ["%s"] + } +} +`, generationCode) +} diff --git a/internal/service/server/server_test.go b/internal/service/server/server_test.go index 512433272..ba4fd0d29 100644 --- a/internal/service/server/server_test.go +++ b/internal/service/server/server_test.go @@ -122,6 +122,47 @@ func TestAccResourceNcloudServer_vpc_basic(t *testing.T) { }) } +func TestAccResourceNcloudServerImageNumber_vpc_basic(t *testing.T) { + var serverInstance serverservice.ServerInstance + testServerName := GetTestServerName() + resourceName := "ncloud_server.server" + specCode := "s2-g3" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories, + CheckDestroy: testAccCheckServerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccServerImageNumberVpcConfig(testServerName, specCode), + Check: resource.ComposeTestCheckFunc(testAccCheckServerExistsWithProvider("ncloud_server.server", &serverInstance, GetTestProvider(true)), + resource.TestMatchResourceAttr(resourceName, "id", regexp.MustCompile(`^\d+$`)), + resource.TestCheckResourceAttr(resourceName, "server_spec_code", specCode), + resource.TestCheckResourceAttr(resourceName, "name", testServerName), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "cpu_count", "2"), + resource.TestMatchResourceAttr(resourceName, "instance_no", regexp.MustCompile(`^\d+$`)), + resource.TestCheckResourceAttr(resourceName, "is_protect_server_termination", "false"), + resource.TestCheckResourceAttr(resourceName, "login_key_name", fmt.Sprintf("%s-key", testServerName)), + // VPC only + resource.TestMatchResourceAttr(resourceName, "subnet_no", regexp.MustCompile(`^\d+$`)), + resource.TestMatchResourceAttr(resourceName, "vpc_no", regexp.MustCompile(`^\d+$`)), + resource.TestCheckResourceAttr(resourceName, "network_interface.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "network_interface.0.order"), + resource.TestCheckResourceAttrSet(resourceName, "network_interface.0.network_interface_no"), + resource.TestCheckResourceAttrSet(resourceName, "network_interface.0.subnet_no"), + resource.TestCheckResourceAttrSet(resourceName, "network_interface.0.private_ip"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccResourceNcloudServer_vpc_networkInterface(t *testing.T) { var serverInstance serverservice.ServerInstance testServerName := GetTestServerName() @@ -362,6 +403,44 @@ func testAccCheckInstanceDestroyWithProvider(s *terraform.State, provider *schem return nil } +func testAccServerImageNumberVpcConfig(testServerName, specCode string) string { + return fmt.Sprintf(` +resource "ncloud_login_key" "loginkey" { + key_name = "%[1]s-key" +} + +resource "ncloud_vpc" "test" { + name = "%[1]s" + ipv4_cidr_block = "10.5.0.0/16" +} + +resource "ncloud_subnet" "test" { + vpc_no = ncloud_vpc.test.vpc_no + name = "%[1]s" + subnet = "10.5.0.0/24" + zone = "KR-2" + network_acl_no = ncloud_vpc.test.default_network_acl_no + subnet_type = "PUBLIC" + usage_type = "GEN" +} + +data "ncloud_server_image_numbers" "server_images" { + filter { + name = "name" + values = ["ubuntu-22.04-base"] + } +} + +resource "ncloud_server" "server" { + subnet_no = ncloud_subnet.test.id + name = "%[1]s" + server_image_number = data.ncloud_server_image_numbers.server_images.image_number_list.0.server_image_number + server_spec_code = "%[2]s" + login_key_name = ncloud_login_key.loginkey.key_name +} +`, testServerName, specCode) +} + func testAccServerVpcConfig(testServerName, productCode string) string { return fmt.Sprintf(` resource "ncloud_login_key" "loginkey" {