From eba225349438d5e253d544fa7b252d74982ce645 Mon Sep 17 00:00:00 2001 From: Lance Date: Wed, 16 Oct 2024 16:06:32 +0800 Subject: [PATCH] feat(servicestage): add new resource to manage component via ver.3 APIs (#5645) * feat(utils): supports two new methods to convert json/string object * chore(utils): ignore method can handle the nil inputs * feat(servicestage/component): add new resource to manage components --- docs/resources/servicestagev3_component.md | 532 ++++++ huaweicloud/provider.go | 1 + ...aweicloud_servicestagev3_component_test.go | 606 +++++++ ...ce_huaweicloud_servicestagev3_component.go | 1456 +++++++++++++++++ huaweicloud/utils/type_convert.go | 36 + huaweicloud/utils/type_convert_test.go | 88 + 6 files changed, 2719 insertions(+) create mode 100644 docs/resources/servicestagev3_component.md create mode 100644 huaweicloud/services/acceptance/servicestage/resource_huaweicloud_servicestagev3_component_test.go create mode 100644 huaweicloud/services/servicestage/resource_huaweicloud_servicestagev3_component.go create mode 100644 huaweicloud/utils/type_convert_test.go diff --git a/docs/resources/servicestagev3_component.md b/docs/resources/servicestagev3_component.md new file mode 100644 index 0000000000..c0e89141f5 --- /dev/null +++ b/docs/resources/servicestagev3_component.md @@ -0,0 +1,532 @@ +--- +subcategory: "ServiceStage" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_servicestagev3_component" +description: |- + Manages a component resource within HuaweiCloud. +--- + +# huaweicloud_servicestagev3_component + +Manages a component resource within HuaweiCloud. + +## Example Usage + +```hcl +variable "application_id" {} +variable "environment_id" {} +variable "component_name" {} +variable "ims_docker_image_url" {} +variable "associated_cce_cluster_id" {} +variable "associated_cse_engine_id" {} + +resource "huaweicloud_servicestagev3_component" "test" { + application_id = var.application_id + environment_id = var.environment_id + name = var.component_name + + runtime_stack { + deploy_mode = "container" + name = "Docker" + type = "Docker" + } + + source = jsonencode({ + "auth": "iam", + "kind": "image", + "storage": "swr", + "url": var.ims_docker_image_url + }) + + version = "1.0.1" + replica = 2 + + refer_resources { + id = var.associated_cce_cluster_id + type = "cce" + parameters = jsonencode({ + "namespace": "default", + "type": "VirtualMachine" + }) + } + refer_resources { + id = var.associated_cse_engine_id + type = "cse" + } + + tags = { + foo = "bar" + } + + description = "Created by terraform script" + limit_cpu = 0.25 + limit_memory = 0.5 + request_cpu = 0.25 + request_memory = 0.5 + + envs { + name = "env_name" + value = "env_value" + } + + storages { + type = "HostPath" + name = "%[2]s" + parameters = jsonencode({ + "default_mode": 0, + "path": "/tmp" + }) + mounts { + path = "/category" + sub_path = "sub" + read_only = false + } + } + + command = jsonencode({ + "args": ["-a"], + "command": ["ls"] + }) + + post_start { + command = ["test"] + type = "command" + } + + pre_stop { + command = ["test"] + type = "command" + } + + mesher { + port = 60 + } + + timezone = "Asia/Shanghai" + + logs { + log_path = "/tmp" + rotate = "Hourly" + host_path = "/tmp" + host_extend_path = "PodName" + } + + custom_metric { + path = "/tmp" + port = 600 + dimensions = "cpu_usage,mem_usage" + } + + affinity { + condition = "required" + kind = "node" + match_expressions { + key = "affinity1" + value = "foo" + operation = "In" + } + weight = 100 + } + affinity { + condition = "preferred" + kind = "node" + match_expressions { + key = "affinity2" + value = "bar" + operation = "NotIn" + } + weight = 1 + } + + anti_affinity { + condition = "required" + kind = "pod" + match_expressions { + key = "anit-affinity1" + operation = "Exists" + } + weight = 100 + } + anti_affinity { + condition = "preferred" + kind = "pod" + match_expressions { + key = "anti-affinity2" + operation = "DoesNotExist" + } + weight = 1 + } + + liveness_probe { + type = "tcp" + delay = 30 + timeout = 30 + port = 800 + } + + readiness_probe { + type = "http" + delay = 30 + timeout = 30 + scheme = "HTTPS" + host = "127.0.0.1" + port = 8000 + path = "/v1/test" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region where the component is located. + If omitted, the provider-level region will be used. Changing this will create a new resource. + +* `application_id` - (Required, String, ForceNew) Specifies the application ID to which the component belongs. + Changing this will create a new resource. + +* `environment_id` - (Required, String, ForceNew) Specifies the environment ID where the component is deployed. + Changing this will create a new resource. + +* `name` - (Required, String, ForceNew) Specifies the name of the component. + The valid length is limited from `2` to `64`, only letters, digits, hyphens (-) and underscores (_) are allowed. + The name must start with a letter and end with a letter or a digit. + Changing this will create a new resource. + +* `runtime_stack` - (Required, List, ForceNew) Specifies the configuration of the runtime stack. + The [runtime_stack](#servicestage_v3_component_runtime_stack) structure is documented below. + Changing this will create a new resource. + +* `source` - (Required, String) Specifies the source configuration of the component, in JSON format. + For the keys, please refer to the [documentation](https://support.huaweicloud.com/intl/en-us/api-servicestage/servicestage_06_0076.html#servicestage_06_0076__en-us_topic_0220056058_ref28944532). + +* `version` - (Required, String) Specifies the version of the component. + The format is **{number}.{number}.{number}** or **{number}.{number}.{number}.{number}**, e.g. **1.0.1**. + +* `replica` - (Required, Int, ForceNew) Specifies the replica number of the component. + Changing this will create a new resource. + +* `refer_resources` - (Required, List) Specifies the configuration of the reference resources. + The [refer_resources](#servicestage_v3_component_refer_resources) structure is documented below. + +* `description` - (Optional, String) Specifies the description of the component. + The value can contain a maximum of `128` characters. + + -> The value of the `description` cannot be set to empty value by updating. + +* `build` - (Optional, String) Specifies the build configuration of the component, in JSON format. + For the keys, please refer to the [documentation](https://support.huaweicloud.com/intl/en-us/api-servicestage/servicestage_06_0076.html#servicestage_06_0076__en-us_topic_0220056060_table7559740). + +* `limit_cpu` - (Optional, Float) Specifies the maximum number of the CPU limit. + The unit is **Core**. + +* `limit_memory` - (Optional, Float) Specifies the maximum number of the memory limit. + The unit is **GiB**. + +* `request_cpu` - (Optional, Float) Specifies the number of the CPU request resources. + The unit is **Core**. + +* `request_memory` - (Optional, Float) Specifies the number of the memory request resources. + The unit is **GiB**. + +* `envs` - (Optional, List) Specifies the configuration of the environment variables. + The [envs](#servicestage_v3_component_envs) structure is documented below. + +* `storages` - (Optional, List) Specifies the storage configuration. + The [storages](#servicestage_v3_component_storages) structure is documented below. + +* `deploy_strategy` - (Optional, List) Specifies the configuration of the deploy strategy. + The [deploy_strategy](#servicestage_v3_component_deploy_strategy) structure is documented below. + +* `command` - (Optional, String) Specifies the start commands of the component, in JSON format. + For the keys, please refer to the [documentation](https://support.huaweicloud.com/intl/en-us/api-servicestage/servicestage_06_0076.html#servicestage_06_0076__table856311795212). + +* `post_start` - (Optional, List) Specifies the post start configuration. + The [post_start](#servicestage_v3_component_lifecycle) structure is documented below. + +* `pre_stop` - (Optional, List) Specifies the pre stop configuration. + The [pre_stop](#servicestage_v3_component_lifecycle) structure is documented below. + +* `mesher` - (Optional, List) Specifies the configuration of the access mesher. + The [mesher](#servicestage_v3_component_mesher) structure is documented below. + +* `timezone` - (Optional, String) Specifies the time zone in which the component runs, e.g. **Asia/Shanghai**. + +* `jvm_opts` - (Optional, String) Specifies the JVM parameters of the component. e.g. **-Xms256m -Xmx1024m**. + If there are multiple parameters, separate them by spaces. + If this parameter is left blank, the default value is used. + +* `tomcat_opts` - (Optional, String) Specifies the configuration of the tomcat server, in JSON format. + For the keys, please refer to the [documentation](https://support.huaweicloud.com/intl/en-us/api-servicestage/servicestage_06_0076.html#servicestage_06_0076__table2836191954317). + +* `logs` - (Optional, List) Specifies the configuration of the logs collection. + The [logs](#servicestage_v3_component_logs) structure is documented below. + +* `custom_metric` - (Optional, List) Specifies the configuration of the monitor metric. + The [custom_metric](#servicestage_v3_component_custom_metric) structure is documented below. + +* `affinity` - (Optional, List) Specifies the affinity configuration of the component. + The [affinity](#servicestage_v3_component_affinity) structure is documented below. + +* `anti_affinity` - (Optional, List) Specifies the anti-affinity configuration of the component. + The [anti_affinity](#servicestage_v3_component_affinity) structure is documented below. + +* `liveness_probe` - (Optional, List) Specifies the liveness probe configuration of the component. + The [liveness_probe](#servicestage_v3_component_probe) structure is documented below. + +* `readiness_probe` - (Optional, List) Specifies the readiness probe configuration of the component. + The [readiness_probe](#servicestage_v3_component_probe) structure is documented below. + +* `external_accesses` - (Optional, List) Specifies the configuration of the external accesses. + The [external_accesses](#servicestage_v3_component_external_accesses) structure is documented below. + +* `tags` - (Optional, Map) Specifies the key/value pairs to associate with the component. + + +The `runtime_stack` block supports: + +* `name` - (Required, String, ForceNew) Specifies the stack name. + Changing this will create a new resource. + +* `type` - (Required, String, ForceNew) Specifies the stack type. + Changing this will create a new resource. + +* `deploy_mode` - (Required, String, ForceNew) Specifies the deploy mode of the stack. + Changing this will create a new resource. + +* `version` - (Optional, String, ForceNew) Specifies the stack version. + Changing this will create a new resource. + + +The `refer_resources` block supports: + +* `id` - (Required, String) Specifies the resource ID. + +* `type` - (Required, String) Specifies the resource type. + +* `parameters` - (Optional, String) Specifies the resource parameters, in JSON format. + For the keys, please refer to the [documentation](https://support.huaweicloud.com/intl/en-us/api-servicestage/servicestage_06_0076.html#servicestage_06_0076__table838321632514). + + +The `envs` block supports: + +* `name` - (Required, String) Specifies the name of the environment variable. + +* `value` - (Optional, String) Specifies the value of the environment variable. + + +The `storages` block supports: + +* `type` - (Required, String) Specifies the type of the data storage. + + **HostPath**: Host path for local disk mounting. + + **EmptyDir**: Temporary directory for local disk mounting. + + **ConfigMap**: Configuration item for local disk mounting. + + **Secret**: Secrets for local disk mounting. + + **PersistentVolumeClaim**: Cloud storage mounting. + +* `name` - (Required, String) Specifies the name of the disk where the data is stored. + Only lowercase letters, digits, and hyphens (-) are allowed and must start and end with a lowercase letter or digit. + +* `parameters` - (Required, String) Specifies the information corresponding to the specific types of data storage, + in JSON format. + For the keys, please refer to the [documentation](https://support.huaweicloud.com/intl/en-us/api-servicestage/servicestage_06_0076.html#servicestage_06_0076__table16441247172510). + +* `mounts` - (Required, List) Specifies the configuration of the disk mounts. + The [mounts](#servicestage_v3_component_storage_mounts) structure is documented below. + + +The `mounts` block supports: + +* `path` - (Required, String) Specifies the mount path. + +* `sub_path` - (Required, String) Specifies the sub mount path. + +* `read_only` - (Required, Bool) Specifies whether the disk mount is read-only. + + +The `deploy_strategy` block supports: + +* `type` - (Required, String) Specifies the deploy type. + + **OneBatchRelease**: Single-batch upgrade. + + **RollingRelease**: Rolling deployment and upgrade. + + **GrayRelease**: Dark launch upgrade. + +* `rolling_release` - (Optional, String) Specifies the rolling release parameters, in JSON format. + Required if the `type` is **RollingRelease**. + For the keys, please refer to the [documentation](https://support.huaweicloud.com/intl/en-us/api-servicestage/servicestage_06_0076.html#servicestage_06_0076__table4696103920). + +* `gray_release` - (Optional, String) Specifies the gray release parameters, in JSON format. + Required if the `type` is **GrayRelease**. + For the keys, please refer to the [documentation](https://support.huaweicloud.com/intl/en-us/api-servicestage/servicestage_06_0076.html#servicestage_06_0076__table888818707). + + +The `post_start` and `pre_stop` blocks support: + +* `type` - (Required, String) Specifies the processing method. + + **http** + + **command** + +* `scheme` - (Optional, String) Specifies the HTTP request type. + + **HTTP** + + **HTTPS** + + This parameter is only available when the `type` is set to `http`. + +* `host` - (Optional, String) Specifies the host (IP) of the lifecycle configuration. + If this parameter is left blank, the pod IP address is used. + This parameter is only available when the `type` is set to `http`. + +* `port` - (Optional, String) Specifies the port number of the lifecycle configuration. + This parameter is only available when the `type` is set to `http`. + +* `path` - (Optional, String) Specifies the request path of the lifecycle configuration. + This parameter is only available when the `type` is set to `http`. + +* `command` - (Optional, List) Specifies the command list of the lifecycle configuration. + This parameter is only available when the `type` is set to `command`. + + +The `mesher` block supports: + +* `port` - (Required, Int) Specifies the process listening port. + + +The `logs` block supports: + +* `log_path` - (Required, String) Specifies the log path of the container, e.g. **/tmp**. + +* `rotate` - (Required, String) Specifies the interval for dumping logs. + + **Hourly** + + **Daily** + + **Weekly** + +* `host_path` - (Required, String) Specifies the mounted host path, e.g. **/tmp**. + +* `host_extend_path` - (Required, String) Specifies the extension path of the host. + + **None**: the extended path is not used. + + **PodUID**: extend the host path based on the pod ID. + + **PodName**: extend the host path based on the pod name. + + **PodUID/ContainerName**: extend the host path based on the pod ID and container name. + + **PodName/ContainerName**: extend the host path based on the pod name and container name. + + +The `custom_metric` block supports: + +* `path` - (Required, String) Specifies the collection path, such as **./metrics**. + +* `port` - (Required, Int) Specifies the collection port, such as **9090**. + +* `dimensions` - (Required, String) Specifies the monitoring dimension, such as **cpu_usage**, **mem_usage** or + **cpu_usage,mem_usage** (separated by a comma). + + +The `affinity` and `anti_affinity` blocks support: + +* `condition` - (Required, String) Specifies the condition type of the (anti) affinity rule. + +* `kind` - (Required, String) Specifies the kind of the (anti) affinity rule. + +* `match_expressions` - (Required, List) Specifies the list of the match rules for (anti) affinity. + The [match_expressions](#servicestage_v3_component_affinity_match_expressions) structure is documented below. + +* `weight` - (Optional, Int) Specifies the weight of the (anti) affinity rule. + The valid value is range from `1` to `100`. + + +The `match_expressions` block supports: + +* `key` - (Required, String) Specifies the key of the match rule. + +* `operation` - (Required, String) Specifies the operation of the match rule. + +* `value` - (Required, String) Specifies the value of the match rule. + + +The `liveness_probe` and `readiness_probe` blocks support: + +* `type` - (Required, String) Specifies the type of the probe. + + **http** + + **tcp** + + **command** + +* `delay` - (Required, Int) Specifies the delay time of the probe. + +* `timeout` - (Required, Int) Specifies the timeout of the probe. + +* `scheme` - (Optional, String) Specifies the scheme type of the probe. + + **HTTP** + + **HTTPS** + + This parameter is only available when the `type` is set to `http`. + +* `host` - (Optional, String) Specifies the host of the probe. + Defaults to pod ID, also custom IP address can be specified. + This parameter is only available when the `type` is set to `http`. + +* `port` - (Optional, Int) Specifies the port of the probe. + This parameter is only available when the `type` is set to `tcp` or `http`. + +* `path` - (Optional, String) Specifies the path of the probe. + This parameter is only available when the `type` is set to `http`. + +* `command` - (Optional, List) Specifies the command list of the probe. + This parameter is only available when the `type` is set to `command`. + + +The `external_accesses` block supports: + +* `protocol` - (Required, String) Specifies the protocol of the external access. + +* `address` - (Optional, String) Specifies the address of the external access. + +* `forward_port` - (Optional, Int) Specifies the forward port of the external access. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID, in UUID format. + +* `status` - The status of the component. + + **RUNNING** + + **PENDING** + +* `created_at` - The creation time of the component, in RFC3339 format. + +* `updated_at` - The latest update time of the component, in RFC3339 format. + +## Timeouts + +This resource provides the following timeouts configuration options: + +* `create` - Default is 20 minutes. +* `update` - Default is 20 minutes. +* `delete` - Default is 5 minutes. + +## Import + +Components can be imported using `application_id` and `id` separated by a slash e.g. + +```bash +$ terraform import huaweicloud_servicestagev3_component.test / +``` + +Note that the imported state may not be identical to your resource definition, due to attributes missing from the API +response, security or some other reason. +The missing attribute is `tags`. +It is generally recommended running `terraform plan` after importing resource. +You can decide if changes should be applied to resource, or the definition should be updated to align with the resource. +Also you can ignore changes as below. + +```hcl +resource "huaweicloud_servicestagev3_component" "test" { + ... + + lifecycle { + ignore_changes = [ + tags, + ] + } +} +``` diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index 372ea80e5c..68fd237089 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1881,6 +1881,7 @@ func Provider() *schema.Provider { "huaweicloud_servicestage_repo_password_authorization": servicestage.ResourceRepoPwdAuth(), // v3 managements "huaweicloud_servicestagev3_application": servicestage.ResourceV3Application(), + "huaweicloud_servicestagev3_component": servicestage.ResourceV3Component(), "huaweicloud_servicestagev3_environment": servicestage.ResourceV3Environment(), "huaweicloud_servicestagev3_environment_associate": servicestage.ResourceV3EnvironmentAssociate(), diff --git a/huaweicloud/services/acceptance/servicestage/resource_huaweicloud_servicestagev3_component_test.go b/huaweicloud/services/acceptance/servicestage/resource_huaweicloud_servicestagev3_component_test.go new file mode 100644 index 0000000000..0fd09dc5a0 --- /dev/null +++ b/huaweicloud/services/acceptance/servicestage/resource_huaweicloud_servicestagev3_component_test.go @@ -0,0 +1,606 @@ +package servicestage + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/servicestage" +) + +func getV3ComponentFunc(conf *config.Config, state *terraform.ResourceState) (interface{}, error) { + client, err := conf.NewServiceClient("servicestage", acceptance.HW_REGION_NAME) + if err != nil { + return nil, fmt.Errorf("error creating ServiceStage client: %s", err) + } + return servicestage.QueryV3Component(client, state.Primary.Attributes["application_id"], state.Primary.ID) +} + +func TestAccV3Component_basic(t *testing.T) { + var ( + component interface{} + + resourceName = "huaweicloud_servicestagev3_component.test" + rc = acceptance.InitResourceCheck(resourceName, &component, getV3ComponentFunc) + + name = acceptance.RandomAccResourceNameWithDash() + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + // Make sure at least one of node exist. + acceptance.TestAccPreCheckCceClusterId(t) + // Make sure the networks of the CCE cluster and the CSE engine are same. + acceptance.TestAccPreCheckCSEMicroserviceEngineID(t) + acceptance.TestAccPreCheckImsImageUrl(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccV3Component_basic_step1(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(resourceName, "application_id", "huaweicloud_servicestagev3_application.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "environment_id", "huaweicloud_servicestagev3_environment.test", "id"), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "runtime_stack.#", "1"), + resource.TestCheckResourceAttr(resourceName, "runtime_stack.0.deploy_mode", "container"), + resource.TestCheckResourceAttr(resourceName, "runtime_stack.0.name", "Docker"), + resource.TestCheckResourceAttr(resourceName, "runtime_stack.0.type", "Docker"), + resource.TestCheckResourceAttr(resourceName, "source", + fmt.Sprintf("{\"auth\":\"iam\",\"kind\":\"image\",\"storage\":\"swr\",\"url\":\"%s\"}", acceptance.HW_IMS_IMAGE_URL)), + resource.TestCheckResourceAttr(resourceName, "version", "1.0.1"), + resource.TestCheckResourceAttr(resourceName, "replica", "2"), + resource.TestCheckResourceAttr(resourceName, "refer_resources.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar"), + resource.TestCheckResourceAttr(resourceName, "description", "Created by terraform script"), + resource.TestCheckResourceAttr(resourceName, "limit_cpu", "0.25"), + resource.TestCheckResourceAttr(resourceName, "limit_memory", "0.5"), + resource.TestCheckResourceAttr(resourceName, "request_cpu", "0.25"), + resource.TestCheckResourceAttr(resourceName, "request_memory", "0.5"), + resource.TestCheckResourceAttr(resourceName, "envs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "envs.0.name", "env_name"), + resource.TestCheckResourceAttr(resourceName, "envs.0.value", "env_value"), + resource.TestCheckResourceAttr(resourceName, "storages.#", "1"), + resource.TestCheckResourceAttr(resourceName, "storages.0.type", "HostPath"), + resource.TestCheckResourceAttr(resourceName, "storages.0.name", name), + resource.TestCheckResourceAttr(resourceName, "storages.0.parameters", "{\"default_mode\":0,\"path\":\"/tmp\"}"), + resource.TestCheckResourceAttr(resourceName, "storages.0.mounts.#", "1"), + resource.TestCheckResourceAttr(resourceName, "storages.0.mounts.0.path", "/category"), + resource.TestCheckResourceAttr(resourceName, "storages.0.mounts.0.sub_path", "sub"), + resource.TestCheckResourceAttr(resourceName, "storages.0.mounts.0.read_only", "false"), + resource.TestCheckResourceAttr(resourceName, "command", "{\"args\":[\"-a\"],\"command\":[\"ls\"]}"), + resource.TestCheckResourceAttr(resourceName, "post_start.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_start.0.command.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_start.0.command.0", "test"), + resource.TestCheckResourceAttr(resourceName, "post_start.0.type", "command"), + resource.TestCheckResourceAttr(resourceName, "pre_stop.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_stop.0.command.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_stop.0.command.0", "test"), + resource.TestCheckResourceAttr(resourceName, "pre_stop.0.type", "command"), + resource.TestCheckResourceAttr(resourceName, "mesher.#", "1"), + resource.TestCheckResourceAttr(resourceName, "mesher.0.port", "60"), + resource.TestCheckResourceAttr(resourceName, "timezone", "Asia/Shanghai"), + resource.TestCheckResourceAttr(resourceName, "logs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "logs.0.log_path", "/tmp"), + resource.TestCheckResourceAttr(resourceName, "logs.0.rotate", "Hourly"), + resource.TestCheckResourceAttr(resourceName, "logs.0.host_path", "/tmp"), + resource.TestCheckResourceAttr(resourceName, "logs.0.host_extend_path", "PodName"), + resource.TestCheckResourceAttr(resourceName, "custom_metric.#", "1"), + resource.TestCheckResourceAttr(resourceName, "custom_metric.0.path", "/tmp"), + resource.TestCheckResourceAttr(resourceName, "custom_metric.0.port", "600"), + resource.TestCheckResourceAttr(resourceName, "custom_metric.0.dimensions", "cpu_usage,mem_usage"), + resource.TestCheckResourceAttr(resourceName, "affinity.#", "2"), + resource.TestCheckResourceAttr(resourceName, "affinity.#", "2"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.#", "1"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.0.type", "tcp"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.0.delay", "30"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.0.timeout", "30"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.0.port", "800"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.#", "1"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.type", "http"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.delay", "30"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.timeout", "30"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.scheme", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.host", "127.0.0.1"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.port", "8000"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.path", "/v1/test"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestMatchResourceAttr(resourceName, "created_at", + regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)), + ), + }, + { + Config: testAccV3Component_basic_step2(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(resourceName, "application_id", "huaweicloud_servicestagev3_application.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "environment_id", "huaweicloud_servicestagev3_environment.test", "id"), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "runtime_stack.#", "1"), + resource.TestCheckResourceAttr(resourceName, "runtime_stack.0.deploy_mode", "container"), + resource.TestCheckResourceAttr(resourceName, "runtime_stack.0.name", "Docker"), + resource.TestCheckResourceAttr(resourceName, "runtime_stack.0.type", "Docker"), + resource.TestCheckResourceAttr(resourceName, "source", + fmt.Sprintf("{\"auth\":\"iam\",\"kind\":\"image\",\"storage\":\"swr\",\"url\":\"%s\"}", acceptance.HW_IMS_IMAGE_URL)), + resource.TestCheckResourceAttr(resourceName, "version", "1.0.2"), + resource.TestCheckResourceAttr(resourceName, "replica", "2"), + resource.TestCheckResourceAttr(resourceName, "refer_resources.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.foo", "baar"), + resource.TestCheckResourceAttr(resourceName, "description", "Updated by terraform script"), + resource.TestCheckResourceAttr(resourceName, "limit_cpu", "0.5"), + resource.TestCheckResourceAttr(resourceName, "limit_memory", "1"), + resource.TestCheckResourceAttr(resourceName, "request_cpu", "0.5"), + resource.TestCheckResourceAttr(resourceName, "request_memory", "1"), + resource.TestCheckResourceAttr(resourceName, "envs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "envs.0.name", "new_env_name"), + resource.TestCheckResourceAttr(resourceName, "envs.0.value", "new_env_value"), + resource.TestCheckResourceAttr(resourceName, "storages.#", "1"), + resource.TestCheckResourceAttr(resourceName, "storages.0.type", "HostPath"), + resource.TestCheckResourceAttr(resourceName, "storages.0.name", fmt.Sprintf("%s-new", name)), + resource.TestCheckResourceAttr(resourceName, "storages.0.parameters", "{\"default_mode\":0,\"path\":\"/tmp/new\"}"), + resource.TestCheckResourceAttr(resourceName, "storages.0.mounts.#", "1"), + resource.TestCheckResourceAttr(resourceName, "storages.0.mounts.0.path", "/category/new"), + resource.TestCheckResourceAttr(resourceName, "storages.0.mounts.0.sub_path", "sub/new"), + resource.TestCheckResourceAttr(resourceName, "storages.0.mounts.0.read_only", "true"), + resource.TestCheckResourceAttr(resourceName, "command", "{\"args\":[\"-l\"],\"command\":[\"ls\"]}"), + resource.TestCheckResourceAttr(resourceName, "post_start.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_start.0.command.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_start.0.command.0", "newtest"), + resource.TestCheckResourceAttr(resourceName, "post_start.0.type", "command"), + resource.TestCheckResourceAttr(resourceName, "pre_stop.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_stop.0.command.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_stop.0.command.0", "newtest"), + resource.TestCheckResourceAttr(resourceName, "pre_stop.0.type", "command"), + resource.TestCheckResourceAttr(resourceName, "mesher.#", "1"), + resource.TestCheckResourceAttr(resourceName, "mesher.0.port", "80"), + resource.TestCheckResourceAttr(resourceName, "timezone", "Asia/HongKong"), + resource.TestCheckResourceAttr(resourceName, "logs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "logs.0.log_path", "/tmp/new"), + resource.TestCheckResourceAttr(resourceName, "logs.0.rotate", "Daily"), + resource.TestCheckResourceAttr(resourceName, "logs.0.host_path", "/tmp/new"), + resource.TestCheckResourceAttr(resourceName, "logs.0.host_extend_path", "PodUID"), + resource.TestCheckResourceAttr(resourceName, "custom_metric.#", "1"), + resource.TestCheckResourceAttr(resourceName, "custom_metric.0.path", "/tmp/new"), + resource.TestCheckResourceAttr(resourceName, "custom_metric.0.port", "800"), + resource.TestCheckResourceAttr(resourceName, "custom_metric.0.dimensions", "mem_usage"), + resource.TestCheckResourceAttr(resourceName, "affinity.#", "2"), + resource.TestCheckResourceAttr(resourceName, "affinity.#", "2"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.#", "1"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.0.type", "tcp"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.0.delay", "60"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.0.timeout", "60"), + resource.TestCheckResourceAttr(resourceName, "liveness_probe.0.port", "900"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.#", "1"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.type", "http"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.delay", "60"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.timeout", "60"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.scheme", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.host", "192.168.0.1"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "readiness_probe.0.path", "/v1/test/new"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestMatchResourceAttr(resourceName, "updated_at", + regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccV3ComponentImportStateIdFunc(resourceName), + ImportStateVerifyIgnore: []string{ + "tags", + }, + }, + }, + }) +} + +func testAccV3ComponentImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + var applicationId, resourceId string + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("the resource (%s) of compnent is not found in the tfstate", resourceName) + } + applicationId = rs.Primary.Attributes["application_id"] + resourceId = rs.Primary.ID + if applicationId == "" || resourceId == "" { + return "", fmt.Errorf("the component ID is not exist or application ID is missing") + } + return fmt.Sprintf("%s/%s", applicationId, resourceId), nil + } +} + +func testAccV3Component_base(name string) string { + return fmt.Sprintf(` +data "huaweicloud_availability_zones" "test" {} + +data "huaweicloud_cce_clusters" "test" { + cluster_id = "%[1]s" +} + +data "huaweicloud_servicestagev3_runtime_stacks" "test" {} + +locals { + docker_runtime_stack = try([for v in data.huaweicloud_servicestagev3_runtime_stacks.test.runtime_stacks: v if + v.type == "Docker" && v.status == "Supported"][0], null) +} + +resource "huaweicloud_servicestagev3_application" "test" { + name = "%[2]s" + enterprise_project_id = "0" +} + +resource "huaweicloud_servicestagev3_environment" "test" { + name = "%[2]s" + vpc_id = try(data.huaweicloud_cce_clusters.test.clusters[0].vpc_id, "") + enterprise_project_id = "0" +} + +resource "huaweicloud_servicestagev3_environment_associate" "test" { + environment_id = huaweicloud_servicestagev3_environment.test.id + + resources { + id = "%[1]s" + type = "cce" + } + resources { + id = "%[3]s" + type = "cse" + } +} +`, acceptance.HW_CCE_CLUSTER_ID, + name, + acceptance.HW_CSE_MICROSERVICE_ENGINE_ID) +} + +func testAccV3Component_basic_step1(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_servicestagev3_component" "test" { + depends_on = [ + huaweicloud_servicestagev3_environment_associate.test + ] + + application_id = huaweicloud_servicestagev3_application.test.id + environment_id = huaweicloud_servicestagev3_environment.test.id + name = "%[2]s" + + runtime_stack { + deploy_mode = try(local.docker_runtime_stack.deploy_mode, null) + name = try(local.docker_runtime_stack.name, null) + type = try(local.docker_runtime_stack.type, null) + version = try(local.docker_runtime_stack.version, null) + } + + source = jsonencode({ + "auth": "iam", + "kind": "image", + "storage": "swr", + "url": "%[3]s" + }) + + version = "1.0.1" + replica = 2 + + refer_resources { + id = "%[4]s" + type = "cce" + parameters = jsonencode({ + "namespace": "default", + "type": "VirtualMachine" + }) + } + refer_resources { + id = "%[5]s" + type = "cse" + } + + tags = { + foo = "bar" + } + + description = "Created by terraform script" + limit_cpu = 0.25 + limit_memory = 0.5 + request_cpu = 0.25 + request_memory = 0.5 + + envs { + name = "env_name" + value = "env_value" + } + + storages { + type = "HostPath" + name = "%[2]s" + parameters = jsonencode({ + "default_mode": 0, + "path": "/tmp" + }) + mounts { + path = "/category" + sub_path = "sub" + read_only = false + } + } + + command = jsonencode({ + "args": ["-a"], + "command": ["ls"] + }) + + post_start { + command = ["test"] + type = "command" + } + + pre_stop { + command = ["test"] + type = "command" + } + + mesher { + port = 60 + } + + timezone = "Asia/Shanghai" + + logs { + log_path = "/tmp" + rotate = "Hourly" + host_path = "/tmp" + host_extend_path = "PodName" + } + + custom_metric { + path = "/tmp" + port = 600 + dimensions = "cpu_usage,mem_usage" + } + + affinity { + condition = "required" + kind = "node" + match_expressions { + key = "affinity1" + value = "foo" + operation = "In" + } + weight = 100 + } + affinity { + condition = "preferred" + kind = "node" + match_expressions { + key = "affinity2" + value = "bar" + operation = "NotIn" + } + weight = 1 + } + + anti_affinity { + condition = "required" + kind = "pod" + match_expressions { + key = "anit-affinity1" + operation = "Exists" + } + weight = 100 + } + anti_affinity { + condition = "preferred" + kind = "pod" + match_expressions { + key = "anti-affinity2" + operation = "DoesNotExist" + } + weight = 1 + } + + liveness_probe { + type = "tcp" + delay = 30 + timeout = 30 + port = 800 + } + + readiness_probe { + type = "http" + delay = 30 + timeout = 30 + scheme = "HTTPS" + host = "127.0.0.1" + port = 8000 + path = "/v1/test" + } +} +`, testAccV3Component_base(name), + name, + acceptance.HW_IMS_IMAGE_URL, + acceptance.HW_CCE_CLUSTER_ID, + acceptance.HW_CSE_MICROSERVICE_ENGINE_ID) +} + +func testAccV3Component_basic_step2(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_servicestagev3_component" "test" { + depends_on = [ + huaweicloud_servicestagev3_environment_associate.test + ] + + application_id = huaweicloud_servicestagev3_application.test.id + environment_id = huaweicloud_servicestagev3_environment.test.id + name = "%[2]s" + + runtime_stack { + deploy_mode = try(local.docker_runtime_stack.deploy_mode, null) + name = try(local.docker_runtime_stack.name, null) + type = try(local.docker_runtime_stack.type, null) + version = try(local.docker_runtime_stack.version, null) + } + + source = jsonencode({ + "auth": "iam", + "kind": "image", + "storage": "swr", + "url": "%[3]s" + }) + + version = "1.0.2" + replica = 2 + + refer_resources { + id = "%[4]s" + type = "cce" + parameters = jsonencode({ + "namespace": "default", + "type": "VirtualMachine" + }) + } + refer_resources { + id = "%[5]s" + type = "cse" + } + + tags = { + foo = "baar" + } + + description = "Updated by terraform script" + limit_cpu = 0.5 + limit_memory = 1 + request_cpu = 0.5 + request_memory = 1 + + envs { + name = "new_env_name" + value = "new_env_value" + } + + storages { + type = "HostPath" + name = "%[2]s-new" + parameters = jsonencode({ + "default_mode": 0, + "path": "/tmp/new" + }) + mounts { + path = "/category/new" + sub_path = "sub/new" + read_only = true + } + } + + command = jsonencode({ + "args": ["-l"], + "command": ["ls"] + }) + + post_start { + command = ["newtest"] + type = "command" + } + + pre_stop { + command = ["newtest"] + type = "command" + } + + mesher { + port = 80 + } + + timezone = "Asia/HongKong" + + logs { + log_path = "/tmp/new" + rotate = "Daily" + host_path = "/tmp/new" + host_extend_path = "PodUID" + } + + custom_metric { + path = "/tmp/new" + port = 800 + dimensions = "mem_usage" + } + + affinity { + condition = "required" + kind = "node" + match_expressions { + key = "new_affinity1" + value = "1" + operation = "Gt" + } + weight = 100 + } + affinity { + condition = "preferred" + kind = "node" + match_expressions { + key = "new_affinity2" + value = "100" + operation = "Lt" + } + weight = 1 + } + + anti_affinity { + condition = "required" + kind = "pod" + match_expressions { + key = "new_anit-affinity1" + operation = "Exists" + } + weight = 100 + } + anti_affinity { + condition = "preferred" + kind = "pod" + match_expressions { + key = "new_anti-affinity2" + operation = "DoesNotExist" + } + weight = 1 + } + + liveness_probe { + type = "tcp" + delay = 60 + timeout = 60 + port = 900 + } + + readiness_probe { + type = "http" + delay = 60 + timeout = 60 + scheme = "HTTP" + host = "192.168.0.1" + port = 8080 + path = "/v1/test/new" + } +} +`, testAccV3Component_base(name), + name, + acceptance.HW_IMS_IMAGE_URL, + acceptance.HW_CCE_CLUSTER_ID, + acceptance.HW_CSE_MICROSERVICE_ENGINE_ID) +} diff --git a/huaweicloud/services/servicestage/resource_huaweicloud_servicestagev3_component.go b/huaweicloud/services/servicestage/resource_huaweicloud_servicestagev3_component.go new file mode 100644 index 0000000000..1b4b8fb5b8 --- /dev/null +++ b/huaweicloud/services/servicestage/resource_huaweicloud_servicestagev3_component.go @@ -0,0 +1,1456 @@ +package servicestage + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/chnsz/golangsdk" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/common" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +var v3ComponentNotFoundCodes = []string{ + "SVCSTG.00100401", +} + +// @API ServiceStage POST /v3/{project_id}/cas/applications/{application_id}/components +// @API ServiceStage GET /v3/{project_id}/cas/jobs/{job_id} +// @API ServiceStage GET /v3/{project_id}/cas/applications/{application_id}/components/{component_id} +// @API ServiceStage PUT /v3/{project_id}/cas/applications/{application_id}/components/{component_id} +// @API ServiceStage DELETE /v3/{project_id}/cas/applications/{application_id}/components/{component_id} +func ResourceV3Component() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceV3ComponentCreate, + ReadContext: resourceV3ComponentRead, + UpdateContext: resourceV3ComponentUpdate, + DeleteContext: resourceV3ComponentDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceV3ComponentImportState, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Description: `The region where the component is located.`, + }, + "application_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The application ID to which the component belongs.`, + }, + "environment_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The environment ID where the component is deployed.`, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The name of the component.`, + }, + "runtime_stack": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The stack name.`, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The stack type.`, + }, + "deploy_mode": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The deploy mode of the stack.`, + }, + "version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Description: `The stack version.`, + }, + }, + }, + Description: "The configuration of the runtime stack.", + }, + "source": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsJSON, + Description: `The source configuration of the component, in JSON format.`, + }, + "version": { + Type: schema.TypeString, + Required: true, + Description: `The version of the component.`, + }, + "replica": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: `The replica number of the component.`, + }, + "refer_resources": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + Description: `The resource ID.`, + }, + "type": { + Type: schema.TypeString, + Required: true, + Description: `The resource type.`, + }, + "parameters": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsJSON, + Description: `The resource parameters, in JSON format.`, + }, + }, + }, + Description: `The configuration of the reference resources.`, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `The description of the component.`, + }, + "build": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsJSON, + Description: `The build configuration of the component, in JSON format.`, + }, + "limit_cpu": { + Type: schema.TypeFloat, + Optional: true, + Description: `The maximum number of the CPU limit.`, + }, + "limit_memory": { + Type: schema.TypeFloat, + Optional: true, + Description: `The maximum number of the memory limit.`, + }, + "request_cpu": { + Type: schema.TypeFloat, + Optional: true, + Description: `The number of the CPU request resources.`, + }, + "request_memory": { + Type: schema.TypeFloat, + Optional: true, + Description: `The number of the memory request resources.`, + }, + "envs": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: `The name of the environment variable.`, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: `The value of the environment variable.`, + }, + }, + }, + Description: "The configuration of the environment variables.", + }, + "storages": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: `The type of the data storage.`, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: `The name of the disk where the data is stored.`, + }, + "parameters": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsJSON, + Description: `The information corresponding to the specific types of data storage, in JSON format.`, + }, + "mounts": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "path": { + Type: schema.TypeString, + Required: true, + Description: `The mount path.`, + }, + "sub_path": { + Type: schema.TypeString, + Required: true, + Description: `The sub mount path.`, + }, + "read_only": { + Type: schema.TypeBool, + Required: true, + Description: `Whether the disk mount is read-only.`, + }, + }, + }, + Description: `The configuration of the disk mounts.`, + }, + }, + }, + Description: "The storage configuration.", + }, + "deploy_strategy": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: `The deploy type.`, + }, + "rolling_release": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsJSON, + Description: `The rolling release parameters, in JSON format.`, + }, + "gray_release": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsJSON, + Description: `The gray release parameters, in JSON format.`, + }, + }, + }, + Description: `The configuration of the deploy strategy.`, + }, + "command": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsJSON, + Description: `The start commands of the component, in JSON format.`, + }, + "post_start": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: componentLifecycleSchema(), + Description: `The post start configuration.`, + }, + "pre_stop": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: componentLifecycleSchema(), + Description: `The pre stop configuration.`, + }, + "mesher": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "port": { + Type: schema.TypeInt, + Required: true, + Description: `The process listening port.`, + }, + }, + }, + Description: `The configuration of the access mesher.`, + }, + "timezone": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `The time zone in which the component runs.`, + }, + "jvm_opts": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `The JVM parameters of the component.`, + }, + "tomcat_opts": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsJSON, + Description: `The configuration of the tomcat server.`, + }, + "logs": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "log_path": { + Type: schema.TypeString, + Required: true, + Description: `The log path of the container.`, + }, + "rotate": { + Type: schema.TypeString, + Required: true, + Description: `The interval for dumping logs.`, + }, + "host_path": { + Type: schema.TypeString, + Required: true, + Description: `The mounted host path.`, + }, + "host_extend_path": { + Type: schema.TypeString, + Required: true, + Description: `The extension path of the host.`, + }, + }, + }, + Description: `The configuration of the logs collection.`, + }, + "custom_metric": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "path": { + Type: schema.TypeString, + Required: true, + Description: `The collection path.`, + }, + "port": { + Type: schema.TypeInt, + Required: true, + Description: `The collection port.`, + }, + "dimensions": { + Type: schema.TypeString, + Required: true, + Description: `The monitoring dimension.`, + }, + }, + }, + Description: `The configuration of the monitor metric.`, + }, + "affinity": { + Type: schema.TypeSet, + Optional: true, + Elem: componentAffinitySchema(), + Description: `The affinity configuration of the component.`, + }, + "anti_affinity": { + Type: schema.TypeSet, + Optional: true, + Elem: componentAffinitySchema(), + Description: `The anti-affinity configuration of the component.`, + }, + "liveness_probe": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: componentProbeSchema(), + Description: "The liveness probe configuration of the component.", + }, + "readiness_probe": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: componentProbeSchema(), + Description: "The readiness probe configuration of the component.", + }, + "external_accesses": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "protocol": { + Type: schema.TypeString, + Required: true, + Description: `The protocol of the external access.`, + }, + "address": { + Type: schema.TypeString, + Optional: true, + Description: `The address of the external access.`, + }, + "forward_port": { + Type: schema.TypeInt, + Optional: true, + Description: `The forward port of the external access.`, + }, + }, + }, + Description: "The configuration of the external accesses.", + }, + "tags": common.TagsSchema( + `The key/value pairs to associate with the component.`, + ), + "status": { + Type: schema.TypeString, + Computed: true, + Description: `The status of the component.`, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: `The creation time of the component, in RFC3339 format.`, + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: `The latest update time of the component, in RFC3339 format.`, + }, + }, + } +} + +func componentLifecycleSchema() *schema.Resource { + sc := schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: `The processing method.`, + }, + "scheme": { + Type: schema.TypeString, + Optional: true, + Description: `The HTTP request type.`, + }, + "host": { + Type: schema.TypeString, + Optional: true, + Description: `The host (IP) of the lifecycle configuration.`, + }, + "port": { + Type: schema.TypeInt, + Optional: true, + Description: `The port number of the lifecycle configuration.`, + }, + "path": { + Type: schema.TypeString, + Optional: true, + Description: `The request path of the lifecycle configuration.`, + }, + "command": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: `The command list of the lifecycle configuration.`, + }, + }, + } + return &sc +} + +func componentAffinitySchema() *schema.Resource { + sc := schema.Resource{ + Schema: map[string]*schema.Schema{ + "condition": { + Type: schema.TypeString, + Required: true, + Description: `The condition type of the (anti) affinity rule.`, + }, + "kind": { + Type: schema.TypeString, + Required: true, + Description: `The kind of the (anti) affinity rule.`, + }, + "match_expressions": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + Description: `The key of the match rule.`, + }, + "operation": { + Type: schema.TypeString, + Required: true, + Description: `The operation of the match rule.`, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: `The value of the match rule.`, + }, + }, + }, + Description: "The list of the match rules for (anti) affinity.", + }, + "weight": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: `The weight of the (anti) affinity rule.`, + }, + }, + } + return &sc +} + +func componentProbeSchema() *schema.Resource { + sc := schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: `The type of the probe.`, + }, + "delay": { + Type: schema.TypeInt, + Required: true, + Description: `The delay time of the probe.`, + }, + "timeout": { + Type: schema.TypeInt, + Required: true, + Description: `The timeout of the probe.`, + }, + "scheme": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `The scheme type of the probe.`, + }, + "host": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `The host of the probe.`, + }, + "port": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: `The port of the probe.`, + }, + "path": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `The path of the probe.`, + }, + "command": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: `The command list of the probe.`, + }, + }, + } + return &sc +} + +func buildV3ComponentRuntimeStackConfig(runtimeStacks []interface{}) map[string]interface{} { + if len(runtimeStacks) < 1 { + return nil + } + + runtimeStack := runtimeStacks[0] + return map[string]interface{}{ + "name": utils.PathSearch("name", runtimeStack, nil), + "type": utils.PathSearch("type", runtimeStack, nil), + "deploy_mode": utils.PathSearch("deploy_mode", runtimeStack, nil), + "version": utils.PathSearch("version", runtimeStack, nil), + } +} + +func buildV3ComponentEnvVariables(variables *schema.Set) []interface{} { + if variables.Len() < 1 { + return nil + } + + result := make([]interface{}, 0, variables.Len()) + for _, variable := range variables.List() { + result = append(result, map[string]interface{}{ + "name": utils.PathSearch("name", variable, nil), + "value": utils.PathSearch("value", variable, nil), + }) + } + + return result +} + +func buildV3ComponentStorageMounts(mounts *schema.Set) []interface{} { + if mounts.Len() < 1 { + return nil + } + + result := make([]interface{}, 0, mounts.Len()) + for _, mount := range mounts.List() { + result = append(result, utils.RemoveNil(map[string]interface{}{ + "path": utils.PathSearch("path", mount, nil), + "sub_path": utils.PathSearch("sub_path", mount, nil), + "read_only": utils.PathSearch("read_only", mount, nil), + })) + } + + return result +} + +func buildV3ComponentStorages(storages *schema.Set) []interface{} { + if storages.Len() < 1 { + return nil + } + + result := make([]interface{}, 0, storages.Len()) + for _, storage := range storages.List() { + result = append(result, map[string]interface{}{ + "type": utils.PathSearch("type", storage, nil), + "name": utils.PathSearch("name", storage, nil), + "parameters": utils.StringToJson(utils.PathSearch("parameters", storage, "").(string)), + "mounts": buildV3ComponentStorageMounts(utils.PathSearch("mounts", storage, schema.NewSet(schema.HashString, nil)).(*schema.Set)), + }) + } + + return result +} + +func buildV3ComponentDeployStrategy(strategies []interface{}) map[string]interface{} { + if len(strategies) < 1 { + return nil + } + + strategy := strategies[0] + return map[string]interface{}{ + "type": utils.PathSearch("type", strategy, nil), + "rolling_release": utils.StringToJson(utils.PathSearch("rolling_release", strategy, "").(string)), + "gray_release": utils.StringToJson(utils.PathSearch("gray_release", strategy, "").(string)), + } +} + +func buildV3ComponentLifecycle(lifecycles []interface{}) map[string]interface{} { + if len(lifecycles) < 1 { + return nil + } + + lifecycle := lifecycles[0] + return map[string]interface{}{ + "type": utils.PathSearch("type", lifecycle, nil), + "scheme": utils.ValueIgnoreEmpty(utils.PathSearch("scheme", lifecycle, nil)), + "host": utils.ValueIgnoreEmpty(utils.PathSearch("host", lifecycle, nil)), + "port": utils.ValueIgnoreEmpty(utils.PathSearch("port", lifecycle, nil)), + "path": utils.ValueIgnoreEmpty(utils.PathSearch("path", lifecycle, nil)), + "command": utils.ValueIgnoreEmpty(utils.ExpandToStringListBySet(utils.PathSearch("command", lifecycle, + schema.NewSet(schema.HashString, nil)).(*schema.Set))), + } +} + +func buildV3ComponentMesher(meshers []interface{}) map[string]interface{} { + if len(meshers) < 1 { + return nil + } + + mesher := meshers[0] + return map[string]interface{}{ + "port": utils.PathSearch("port", mesher, nil), + } +} + +func buildV3ComponentLogs(logs *schema.Set) []interface{} { + if logs.Len() < 1 { + return nil + } + + result := make([]interface{}, 0, logs.Len()) + for _, v := range logs.List() { + result = append(result, map[string]interface{}{ + "log_path": utils.PathSearch("log_path", v, nil), + "rotate": utils.PathSearch("rotate", v, nil), + "host_path": utils.PathSearch("host_path", v, nil), + "host_extend_path": utils.PathSearch("host_extend_path", v, nil), + }) + } + + return result +} + +func buildV3ComponentCustomMetric(metrics []interface{}) map[string]interface{} { + if len(metrics) < 1 { + return nil + } + + metric := metrics[0] + return map[string]interface{}{ + "path": utils.ValueIgnoreEmpty(utils.PathSearch("path", metric, nil)), + "port": utils.ValueIgnoreEmpty(utils.PathSearch("port", metric, nil)), + "dimensions": utils.ValueIgnoreEmpty(utils.PathSearch("dimensions", metric, nil)), + } +} + +func buildV3ComponentAffinityMatchExpressions(matchRules *schema.Set) []interface{} { + if matchRules.Len() < 1 { + return nil + } + + result := make([]interface{}, 0, matchRules.Len()) + for _, rule := range matchRules.List() { + result = append(result, map[string]interface{}{ + "key": utils.PathSearch("key", rule, nil), + "operation": utils.PathSearch("operation", rule, nil), + "value": utils.PathSearch("value", rule, nil), + }) + } + + return result +} + +func buildV3ComponentAffinity(affinityRules *schema.Set) []interface{} { + if affinityRules.Len() < 1 { + return nil + } + + result := make([]interface{}, 0, affinityRules.Len()) + for _, rule := range affinityRules.List() { + result = append(result, map[string]interface{}{ + "condition": utils.PathSearch("condition", rule, nil), + "kind": utils.PathSearch("kind", rule, nil), + "match_expressions": utils.ValueIgnoreEmpty(buildV3ComponentAffinityMatchExpressions(utils.PathSearch("match_expressions", rule, + schema.NewSet(schema.HashString, nil)).(*schema.Set))), + "weight": utils.PathSearch("weight", rule, nil), + }) + } + + return result +} + +func buildV3ComponentProbeConfiguration(probeConfigs []interface{}) map[string]interface{} { + if len(probeConfigs) < 1 { + return nil + } + + probeConfig := probeConfigs[0] + return map[string]interface{}{ + "type": utils.ValueIgnoreEmpty(utils.PathSearch("type", probeConfig, nil)), + "delay": utils.ValueIgnoreEmpty(utils.PathSearch("delay", probeConfig, nil)), + "timeout": utils.ValueIgnoreEmpty(utils.PathSearch("timeout", probeConfig, nil)), + "scheme": utils.ValueIgnoreEmpty(utils.PathSearch("scheme", probeConfig, nil)), + "host": utils.ValueIgnoreEmpty(utils.PathSearch("host", probeConfig, nil)), + "port": utils.ValueIgnoreEmpty(utils.PathSearch("port", probeConfig, nil)), + "path": utils.ValueIgnoreEmpty(utils.PathSearch("path", probeConfig, nil)), + "command": utils.ValueIgnoreEmpty(utils.ExpandToStringListBySet(utils.PathSearch("command", + probeConfig, schema.NewSet(schema.HashString, nil)).(*schema.Set))), + } +} + +func buildV3ComponentReferResources(refResources *schema.Set) []interface{} { + if refResources.Len() < 1 { + return nil + } + + result := make([]interface{}, 0, refResources.Len()) + for _, refRsource := range refResources.List() { + result = append(result, utils.RemoveNil(map[string]interface{}{ + "id": utils.ValueIgnoreEmpty(utils.PathSearch("id", refRsource, nil)), + "type": utils.ValueIgnoreEmpty(utils.PathSearch("type", refRsource, nil)), + "parameters": utils.StringToJson(utils.PathSearch("parameters", refRsource, "").(string)), + })) + } + + return result +} + +func buildV3ComponentExternalAccesses(accesses *schema.Set) []interface{} { + if accesses.Len() < 1 { + return nil + } + + result := make([]interface{}, 0, accesses.Len()) + for _, access := range accesses.List() { + result = append(result, utils.RemoveNil(map[string]interface{}{ + "protocol": utils.PathSearch("protocol", access, nil), + "address": utils.ValueIgnoreEmpty(utils.PathSearch("address", access, nil)), + "forward_port": utils.ValueIgnoreEmpty(utils.PathSearch("forward_port", access, nil)), + })) + } + + return result +} + +func buildV3ComponentCreateBodyParams(d *schema.ResourceData) map[string]interface{} { + return map[string]interface{}{ + // Required parameters. + "name": d.Get("name").(string), + "runtime_stack": utils.ValueIgnoreEmpty(buildV3ComponentRuntimeStackConfig(d.Get("runtime_stack").([]interface{}))), + "source": utils.StringToJson(d.Get("source").(string)), + "version": d.Get("version").(string), + "replica": d.Get("replica").(int), + "refer_resources": utils.ValueIgnoreEmpty(buildV3ComponentReferResources(d.Get("refer_resources").(*schema.Set))), + // Optional parameters. + "environment_id": d.Get("environment_id").(string), + "description": utils.ValueIgnoreEmpty(d.Get("description")), + "build": utils.StringToJson(d.Get("build").(string)), + "limit_cpu": d.Get("limit_cpu").(float64), + "limit_memory": d.Get("limit_memory").(float64), + "request_cpu": d.Get("request_cpu").(float64), + "request_memory": d.Get("request_memory").(float64), + "envs": utils.ValueIgnoreEmpty(buildV3ComponentEnvVariables(d.Get("envs").(*schema.Set))), + "storages": utils.ValueIgnoreEmpty(buildV3ComponentStorages(d.Get("storages").(*schema.Set))), + "deploy_strategy": utils.ValueIgnoreEmpty(buildV3ComponentDeployStrategy(d.Get("deploy_strategy").([]interface{}))), + "command": utils.StringToJson(d.Get("command").(string)), + "post_start": utils.ValueIgnoreEmpty(buildV3ComponentLifecycle(d.Get("post_start").([]interface{}))), + "pre_stop": utils.ValueIgnoreEmpty(buildV3ComponentLifecycle(d.Get("pre_stop").([]interface{}))), + "mesher": utils.ValueIgnoreEmpty(buildV3ComponentMesher(d.Get("mesher").([]interface{}))), + "timezone": utils.ValueIgnoreEmpty(d.Get("timezone").(string)), + "jvm_opts": utils.ValueIgnoreEmpty(d.Get("jvm_opts").(string)), + "tomcat_opts": utils.StringToJson(d.Get("tomcat_opts").(string)), + "logs": utils.ValueIgnoreEmpty(buildV3ComponentLogs(d.Get("logs").(*schema.Set))), + "custom_metric": utils.ValueIgnoreEmpty(buildV3ComponentCustomMetric(d.Get("custom_metric").([]interface{}))), + "affinity": utils.ValueIgnoreEmpty(buildV3ComponentAffinity(d.Get("affinity").(*schema.Set))), + "anti_affinity": utils.ValueIgnoreEmpty(buildV3ComponentAffinity(d.Get("anti_affinity").(*schema.Set))), + "liveness_probe": utils.ValueIgnoreEmpty(buildV3ComponentProbeConfiguration(d.Get("liveness_probe").([]interface{}))), + "readiness_probe": utils.ValueIgnoreEmpty(buildV3ComponentProbeConfiguration(d.Get("readiness_probe").([]interface{}))), + "external_accesses": utils.ValueIgnoreEmpty(buildV3ComponentExternalAccesses(d.Get("external_accesses").(*schema.Set))), + "labels": utils.ExpandResourceTagsMap(d.Get("tags").(map[string]interface{})), + } +} + +func queryV3Job(client *golangsdk.ServiceClient, jobId string) (interface{}, error) { + httpUrl := "v3/{project_id}/cas/jobs/{job_id}" + + queryPath := client.Endpoint + httpUrl + queryPath = strings.ReplaceAll(queryPath, "{project_id}", client.ProjectID) + queryPath = strings.ReplaceAll(queryPath, "{job_id}", jobId) + + opt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "Content-Type": "application/json", + }, + } + + requestResp, err := client.Request("GET", queryPath, &opt) + if err != nil { + return nil, err + } + + return utils.FlattenResponse(requestResp) +} + +func jobStatusRefreshFunc(client *golangsdk.ServiceClient, jobId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + respBody, err := queryV3Job(client, jobId) + if err != nil { + return respBody, "ERROR", err + } + + jobStatus := utils.PathSearch("job.execution_status", respBody, "").(string) + if utils.StrSliceContains([]string{"FAILED", "UNKNOWN"}, jobStatus) { + return nil, "ERROR", fmt.Errorf("unexpected status: %s", jobStatus) + } + if utils.StrSliceContains([]string{"SUCCEEDED"}, jobStatus) { + return respBody, "COMPLETED", nil + } + return "continue", "PENDING", nil + } +} + +func waitV3JobCompleted(ctx context.Context, client *golangsdk.ServiceClient, jobId string, timeout time.Duration) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING"}, + Target: []string{"COMPLETED"}, + Refresh: jobStatusRefreshFunc(client, jobId), + Timeout: timeout, + PollInterval: 10 * time.Second, + } + _, err := stateConf.WaitForStateContext(ctx) + if err != nil { + return fmt.Errorf("error waiting for the job (in ServiceStage service) to complete: %s", err) + } + return nil +} + +func resourceV3ComponentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + httpUrl = "v3/{project_id}/cas/applications/{application_id}/components" + appId = d.Get("application_id").(string) + ) + client, err := cfg.NewServiceClient("servicestage", region) + if err != nil { + return diag.Errorf("error creating ServiceStage client: %s", err) + } + + createPath := client.Endpoint + httpUrl + createPath = strings.ReplaceAll(createPath, "{project_id}", client.ProjectID) + createPath = strings.ReplaceAll(createPath, "{application_id}", appId) + + opt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "Content-Type": "application/json", + }, + JSONBody: utils.RemoveNil(buildV3ComponentCreateBodyParams(d)), + } + + requestResp, err := client.Request("POST", createPath, &opt) + if err != nil { + return diag.Errorf("error creating component: %s", err) + } + + respBody, err := utils.FlattenResponse(requestResp) + if err != nil { + return diag.FromErr(err) + } + + componentId := utils.PathSearch("component_id", respBody, "").(string) + if componentId == "" { + return diag.Errorf("unable to find the component ID from the API response") + } + d.SetId(componentId) + + jobId := utils.PathSearch("job_id", respBody, "").(string) + if jobId == "" { + return diag.Errorf("unable to find the job ID of the component creating operation from the API response") + } + err = waitV3JobCompleted(ctx, client, jobId, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return diag.FromErr(err) + } + + return resourceV3ComponentRead(ctx, d, meta) +} + +func QueryV3Component(client *golangsdk.ServiceClient, applicationId, componentId string) (interface{}, error) { + httpUrl := "v3/{project_id}/cas/applications/{application_id}/components/{component_id}" + + queryPath := client.Endpoint + httpUrl + queryPath = strings.ReplaceAll(queryPath, "{project_id}", client.ProjectID) + queryPath = strings.ReplaceAll(queryPath, "{application_id}", applicationId) + queryPath = strings.ReplaceAll(queryPath, "{component_id}", componentId) + + opt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "Content-Type": "application/json", + }, + } + + requestResp, err := client.Request("GET", queryPath, &opt) + if err != nil { + return nil, common.ConvertExpected401ErrInto404Err(err, "error_code", v3ComponentNotFoundCodes...) + } + + return utils.FlattenResponse(requestResp) +} + +func flattenV3ComponentRuntimeStackConfig(runtimeStack map[string]interface{}) []map[string]interface{} { + if len(runtimeStack) < 1 { + return nil + } + + return []map[string]interface{}{ + { + "name": utils.PathSearch("name", runtimeStack, nil), + "type": utils.PathSearch("type", runtimeStack, nil), + "version": utils.PathSearch("version", runtimeStack, nil), + "deploy_mode": utils.PathSearch("deploy_mode", runtimeStack, nil), + }, + } +} + +func flattenV3ComponentEnvVariables(variables []interface{}) []interface{} { + if len(variables) < 1 { + return nil + } + + result := make([]interface{}, 0, len(variables)) + for _, variable := range variables { + envName := utils.PathSearch("name", variable, "").(string) + if utils.IsStrContainsSliceElement(envName, []string{"TZ"}, true, true) { + continue + } + result = append(result, map[string]interface{}{ + "name": envName, + "value": utils.PathSearch("value", variable, nil), + }) + } + + return result +} + +func flattenV3ComponentStorageMounts(mounts []interface{}) []interface{} { + if len(mounts) < 1 { + return nil + } + + result := make([]interface{}, 0, len(mounts)) + for _, mount := range mounts { + result = append(result, map[string]interface{}{ + "path": utils.PathSearch("path", mount, nil), + "sub_path": utils.PathSearch("sub_path", mount, nil), + "read_only": utils.PathSearch("read_only", mount, nil), + }) + } + + return result +} + +func flattenV3ComponentStorages(storages []interface{}) []interface{} { + if len(storages) < 1 { + return nil + } + + result := make([]interface{}, 0, len(storages)) + for _, storage := range storages { + result = append(result, map[string]interface{}{ + "type": utils.PathSearch("type", storage, nil), + "name": utils.PathSearch("name", storage, nil), + "parameters": utils.JsonToString(utils.PathSearch("parameters", storage, nil)), + "mounts": flattenV3ComponentStorageMounts(utils.PathSearch("mounts", storage, make([]interface{}, 0)).([]interface{})), + }) + } + + return result +} + +func flattenV3ComponentDeployStrategy(strategy map[string]interface{}) []map[string]interface{} { + if len(strategy) < 1 { + return nil + } + + return []map[string]interface{}{ + { + "type": utils.PathSearch("type", strategy, nil), + "rolling_release": utils.PathSearch("rolling_release", strategy, nil), + "gray_release": utils.PathSearch("gray_release", strategy, nil), + }, + } +} + +func flattenV3ComponentLifecycle(lifecycle map[string]interface{}) []map[string]interface{} { + if len(lifecycle) < 1 { + return nil + } + + return []map[string]interface{}{ + { + "type": utils.PathSearch("type", lifecycle, nil), + "scheme": utils.PathSearch("scheme", lifecycle, nil), + "host": utils.PathSearch("host", lifecycle, nil), + "port": utils.PathSearch("port", lifecycle, nil), + "path": utils.PathSearch("path", lifecycle, nil), + "command": utils.PathSearch("command", lifecycle, nil), + }, + } +} + +func flattenV3ComponentMesher(mesher map[string]interface{}) []map[string]interface{} { + if len(mesher) < 1 { + return nil + } + + return []map[string]interface{}{ + { + "port": utils.PathSearch("port", mesher, nil), + }, + } +} + +func flattenV3ComponentCustomMetric(customMetric map[string]interface{}) []map[string]interface{} { + if len(customMetric) < 1 { + return nil + } + + return []map[string]interface{}{ + { + "path": utils.PathSearch("path", customMetric, nil), + "port": utils.PathSearch("port", customMetric, nil), + "dimensions": utils.PathSearch("dimensions", customMetric, nil), + }, + } +} + +func flattenV3ComponentAffinityMatchExpressions(matchRules []interface{}) []map[string]interface{} { + if len(matchRules) < 1 { + return nil + } + + result := make([]map[string]interface{}, 0, len(matchRules)) + for _, rule := range matchRules { + result = append(result, map[string]interface{}{ + "key": utils.PathSearch("key", rule, nil), + "operation": utils.PathSearch("operation", rule, nil), + "value": utils.PathSearch("value", rule, nil), + }) + } + + return result +} + +func flattenV3ComponentAffinity(affinityRules []interface{}) []map[string]interface{} { + if len(affinityRules) < 1 { + return nil + } + + result := make([]map[string]interface{}, 0, len(affinityRules)) + for _, rule := range affinityRules { + result = append(result, map[string]interface{}{ + "condition": utils.PathSearch("condition", rule, nil), + "kind": utils.PathSearch("kind", rule, nil), + "match_expressions": flattenV3ComponentAffinityMatchExpressions(utils.PathSearch("match_expressions", + rule, make([]interface{}, 0)).([]interface{})), + "weight": int(utils.PathSearch("weight", rule, float64(0)).(float64)), + }) + } + + return result +} + +func flattenV3ComponentProbe(probe map[string]interface{}) []map[string]interface{} { + if len(probe) < 1 { + return nil + } + + return []map[string]interface{}{ + { + "type": utils.PathSearch("type", probe, nil), + "delay": utils.PathSearch("delay", probe, nil), + "timeout": utils.PathSearch("timeout", probe, nil), + "scheme": utils.PathSearch("scheme", probe, nil), + "host": utils.PathSearch("host", probe, nil), + "port": utils.PathSearch("port", probe, nil), + "path": utils.PathSearch("path", probe, nil), + "command": utils.PathSearch("command", probe, make([]interface{}, 0)), + }, + } +} + +func flattenV3ComponentReferResources(refResources []interface{}) []map[string]interface{} { + if len(refResources) < 1 { + return nil + } + + result := make([]map[string]interface{}, 0, len(refResources)) + for _, refRsource := range refResources { + result = append(result, map[string]interface{}{ + "id": utils.PathSearch("id", refRsource, nil), + "type": utils.PathSearch("type", refRsource, nil), + "parameters": utils.JsonToString(utils.PathSearch("parameters", refRsource, nil)), + }) + } + + return result +} + +func flattenV3ExternalAccesses(access map[string]interface{}) []map[string]interface{} { + if len(access) < 1 { + return nil + } + + return []map[string]interface{}{ + { + "protocol": utils.PathSearch("protocol", access, nil), + "address": utils.PathSearch("address", access, nil), + "forward_port": utils.PathSearch("forward_port", access, nil), + }, + } +} + +func resourceV3ComponentRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + appId = d.Get("application_id").(string) + componentId = d.Id() + ) + client, err := cfg.NewServiceClient("servicestage", region) + if err != nil { + return diag.Errorf("error creating ServiceStage client: %s", err) + } + + respBody, err := QueryV3Component(client, appId, componentId) + if err != nil { + return common.CheckDeletedDiag(d, err, + fmt.Sprintf("error getting component (%s)", componentId)) + } + + mErr := multierror.Append(nil, + d.Set("region", region), + d.Set("name", utils.PathSearch("name", respBody, nil)), + d.Set("runtime_stack", flattenV3ComponentRuntimeStackConfig(utils.PathSearch("runtime_stack", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("environment_id", utils.PathSearch("environment_id", respBody, nil)), + d.Set("description", utils.PathSearch("description", respBody, nil)), + d.Set("source", utils.JsonToString(utils.PathSearch("source", respBody, nil))), + d.Set("build", utils.JsonToString(utils.PathSearch("build", respBody, nil))), + d.Set("limit_cpu", utils.PathSearch("limit_cpu", respBody, nil)), + d.Set("limit_memory", utils.PathSearch("limit_memory", respBody, nil)), + d.Set("request_cpu", utils.PathSearch("request_cpu", respBody, nil)), + d.Set("request_memory", utils.PathSearch("request_memory", respBody, nil)), + d.Set("version", utils.PathSearch("version", respBody, nil)), + d.Set("envs", flattenV3ComponentEnvVariables(utils.PathSearch("envs[?!inner]", respBody, make([]interface{}, 0)).([]interface{}))), + d.Set("replica", utils.PathSearch("replica", respBody, nil)), + d.Set("storages", flattenV3ComponentStorages(utils.PathSearch("storages", respBody, make([]interface{}, 0)).([]interface{}))), + d.Set("deploy_strategy", flattenV3ComponentDeployStrategy(utils.PathSearch("deploy_strategy", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("command", utils.JsonToString(utils.PathSearch("command", respBody, nil))), + d.Set("post_start", flattenV3ComponentLifecycle(utils.PathSearch("post_start", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("pre_stop", flattenV3ComponentLifecycle(utils.PathSearch("pre_stop", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("mesher", flattenV3ComponentMesher(utils.PathSearch("mesher", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("timezone", utils.PathSearch("timezone", respBody, nil)), + d.Set("jvm_opts", utils.PathSearch("jvm_opts", respBody, nil)), + d.Set("tomcat_opts", utils.JsonToString(utils.PathSearch("tomcat_opts", respBody, nil))), + d.Set("logs", utils.PathSearch("logs", respBody, nil)), + d.Set("custom_metric", flattenV3ComponentCustomMetric(utils.PathSearch("custom_metric", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("affinity", flattenV3ComponentAffinity(utils.PathSearch("affinity", respBody, + make([]interface{}, 0)).([]interface{}))), + d.Set("anti_affinity", flattenV3ComponentAffinity(utils.PathSearch("anti_affinity", respBody, + make([]interface{}, 0)).([]interface{}))), + d.Set("liveness_probe", flattenV3ComponentProbe(utils.PathSearch("liveness_probe", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("readiness_probe", flattenV3ComponentProbe(utils.PathSearch("readiness_probe", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("refer_resources", flattenV3ComponentReferResources(utils.PathSearch("refer_resources", respBody, + make([]interface{}, 0)).([]interface{}))), + d.Set("external_accesses", flattenV3ExternalAccesses(utils.PathSearch("external_accesses", respBody, + make(map[string]interface{})).(map[string]interface{}))), + d.Set("status", utils.PathSearch("status.component_status", respBody, nil)), + d.Set("created_at", utils.FormatTimeStampRFC3339(int64(utils.PathSearch("status.create_time", respBody, + float64(0)).(float64))/1000, false)), + d.Set("updated_at", utils.FormatTimeStampRFC3339(int64(utils.PathSearch("status.update_time", respBody, + float64(0)).(float64))/1000, false)), + ) + + return diag.FromErr(mErr.ErrorOrNil()) +} + +func buildV3ComponentUpdteBodyParams(d *schema.ResourceData) map[string]interface{} { + return map[string]interface{}{ + // Cannot be updated but the request body needs them. + "name": d.Get("name").(string), + "runtime_stack": utils.ValueIgnoreEmpty(buildV3ComponentRuntimeStackConfig(d.Get("runtime_stack").([]interface{}))), + "replica": d.Get("replica").(int), + // Required parameters + "source": utils.StringToJson(d.Get("source").(string)), + "version": d.Get("version").(string), + "refer_resources": utils.ValueIgnoreEmpty(buildV3ComponentReferResources(d.Get("refer_resources").(*schema.Set))), + // Optional parameters. + "description": d.Get("description").(string), + "build": utils.StringToJson(d.Get("build").(string)), + "limit_cpu": d.Get("limit_cpu").(float64), + "limit_memory": d.Get("limit_memory").(float64), + "request_cpu": d.Get("request_cpu").(float64), + "request_memory": d.Get("request_memory").(float64), + "envs": utils.ValueIgnoreEmpty(buildV3ComponentEnvVariables(d.Get("envs").(*schema.Set))), + "storages": utils.ValueIgnoreEmpty(buildV3ComponentStorages(d.Get("storages").(*schema.Set))), + "deploy_strategy": utils.ValueIgnoreEmpty(buildV3ComponentDeployStrategy(d.Get("deploy_strategy").([]interface{}))), + "command": utils.StringToJson(d.Get("command").(string)), + "post_start": utils.ValueIgnoreEmpty(buildV3ComponentLifecycle(d.Get("post_start").([]interface{}))), + "pre_stop": utils.ValueIgnoreEmpty(buildV3ComponentLifecycle(d.Get("pre_stop").([]interface{}))), + "mesher": utils.ValueIgnoreEmpty(buildV3ComponentMesher(d.Get("mesher").([]interface{}))), + "timezone": utils.ValueIgnoreEmpty(d.Get("timezone").(string)), + "jvm_opts": utils.ValueIgnoreEmpty(d.Get("jvm_opts").(string)), + "tomcat_opts": utils.StringToJson(d.Get("tomcat_opts").(string)), + "logs": utils.ValueIgnoreEmpty(buildV3ComponentLogs(d.Get("logs").(*schema.Set))), + "custom_metric": utils.ValueIgnoreEmpty(buildV3ComponentCustomMetric(d.Get("custom_metric").([]interface{}))), + "affinity": utils.ValueIgnoreEmpty(buildV3ComponentAffinity(d.Get("affinity").(*schema.Set))), + "anti_affinity": utils.ValueIgnoreEmpty(buildV3ComponentAffinity(d.Get("anti_affinity").(*schema.Set))), + "liveness_probe": utils.ValueIgnoreEmpty(buildV3ComponentProbeConfiguration(d.Get("liveness_probe").([]interface{}))), + "readiness_probe": utils.ValueIgnoreEmpty(buildV3ComponentProbeConfiguration(d.Get("readiness_probe").([]interface{}))), + "external_accesses": utils.ValueIgnoreEmpty(buildV3ComponentExternalAccesses(d.Get("external_accesses").(*schema.Set))), + "labels": utils.ExpandResourceTagsMap(d.Get("tags").(map[string]interface{})), + } +} + +func componentStatusRefreshFunc(client *golangsdk.ServiceClient, appId, commponetId string, targets []string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + respBody, err := QueryV3Component(client, appId, commponetId) + if err != nil { + if _, ok := err.(golangsdk.ErrDefault404); ok && len(targets) < 1 { + log.Printf("[DEBUG] The component (%s) does not exist", commponetId) + return "Resource Not Found", "COMPLETED", nil + } + return respBody, "ERROR", err + } + + componentStatus := utils.PathSearch("status.component_status", respBody, "").(string) + if utils.IsStrContainsSliceElement(componentStatus, []string{"FAILED", "UNKNOWN", "PARTIALLY_FAILED"}, true, true) { + return nil, "ERROR", fmt.Errorf("unexpected status: %s", componentStatus) + } + if utils.IsStrContainsSliceElement(componentStatus, targets, true, true) { + return respBody, "COMPLETED", nil + } + return "continue", "PENDING", nil + } +} + +func waitV3ComponentUpdateCompleted(ctx context.Context, client *golangsdk.ServiceClient, d *schema.ResourceData) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING"}, + Target: []string{"COMPLETED"}, + Refresh: componentStatusRefreshFunc(client, d.Get("application_id").(string), d.Id(), []string{"PENDING", "RUNNING"}), + Timeout: d.Timeout(schema.TimeoutUpdate), + PollInterval: 10 * time.Second, + } + _, err := stateConf.WaitForStateContext(ctx) + if err != nil { + return fmt.Errorf("error waiting for the update operation to complete: %s", err) + } + return nil +} + +func resourceV3ComponentUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + httpUrl = "v3/{project_id}/cas/applications/{application_id}/components/{component_id}" + appId = d.Get("application_id").(string) + componentId = d.Id() + ) + client, err := cfg.NewServiceClient("servicestage", region) + if err != nil { + return diag.Errorf("error creating ServiceStage client: %s", err) + } + + createPath := client.Endpoint + httpUrl + createPath = strings.ReplaceAll(createPath, "{project_id}", client.ProjectID) + createPath = strings.ReplaceAll(createPath, "{application_id}", appId) + createPath = strings.ReplaceAll(createPath, "{component_id}", componentId) + + opt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "Content-Type": "application/json", + }, + JSONBody: utils.RemoveNil(buildV3ComponentUpdteBodyParams(d)), + } + + _, err = client.Request("PUT", createPath, &opt) + if err != nil { + return diag.Errorf("error updating component (%s): %s", componentId, err) + } + + err = waitV3ComponentUpdateCompleted(ctx, client, d) + if err != nil { + return diag.FromErr(err) + } + return resourceV3ComponentRead(ctx, d, meta) +} + +func waitV3ComponentDeleteCompleted(ctx context.Context, client *golangsdk.ServiceClient, d *schema.ResourceData) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING"}, + Target: []string{"COMPLETED"}, + Refresh: componentStatusRefreshFunc(client, d.Get("application_id").(string), d.Id(), nil), + Timeout: d.Timeout(schema.TimeoutDelete), + PollInterval: 10 * time.Second, + } + _, err := stateConf.WaitForStateContext(ctx) + if err != nil { + return fmt.Errorf("error waiting for the delete operation to complete: %s", err) + } + return nil +} + +func resourceV3ComponentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + httpUrl = "v3/{project_id}/cas/applications/{application_id}/components/{component_id}" + appId = d.Get("application_id").(string) + componentId = d.Id() + ) + + client, err := cfg.NewServiceClient("servicestage", region) + if err != nil { + return diag.Errorf("error creating ServiceStage client: %s", err) + } + + deletePath := client.Endpoint + httpUrl + deletePath = strings.ReplaceAll(deletePath, "{project_id}", client.ProjectID) + deletePath = strings.ReplaceAll(deletePath, "{application_id}", appId) + deletePath = strings.ReplaceAll(deletePath, "{component_id}", componentId) + + opt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "Content-Type": "application/json", + }, + } + + // Returns the state code 200 and structure (with format '{"component_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}'). + _, err = client.Request("DELETE", deletePath, &opt) + if err != nil { + return common.CheckDeletedDiag(d, common.ConvertExpected401ErrInto404Err(err, "error_code", v3ComponentNotFoundCodes...), + fmt.Sprintf("error deleting component (%s)", componentId)) + } + + return diag.FromErr(waitV3ComponentDeleteCompleted(ctx, client, d)) +} + +func resourceV3ComponentImportState(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + importedId := d.Id() + parts := strings.SplitN(importedId, "/", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid format specified for import ID, want '/', but got '%s'", importedId) + } + + d.SetId(parts[1]) + return []*schema.ResourceData{d}, d.Set("application_id", parts[0]) +} diff --git a/huaweicloud/utils/type_convert.go b/huaweicloud/utils/type_convert.go index 40ee1e2716..9f960e02e6 100644 --- a/huaweicloud/utils/type_convert.go +++ b/huaweicloud/utils/type_convert.go @@ -1,6 +1,7 @@ package utils import ( + "encoding/json" "log" "reflect" "strconv" @@ -100,7 +101,17 @@ func StringValue(v *string) string { // ValueIgnoreEmpty returns to the string value. if v is empty, return nil func ValueIgnoreEmpty(v interface{}) interface{} { + if v == nil { + return nil + } + vl := reflect.ValueOf(v) + + if !vl.IsValid() { + log.Printf("[ERROR] The value (%#v) is invalid", v) + return nil + } + if (vl.Kind() != reflect.Bool) && vl.IsZero() { return nil } @@ -111,3 +122,28 @@ func ValueIgnoreEmpty(v interface{}) interface{} { return v } + +// Try to parse the string value as the JSON format, if the operation failed, returns an empty map result. +func StringToJson(jsonStrObj string) interface{} { + if jsonStrObj == "" { + return nil + } + jsonMap := make(map[string]interface{}) + err := json.Unmarshal([]byte(jsonStrObj), &jsonMap) + if err != nil { + log.Printf("[ERROR] Unable to convert the JSON string to the map object: %s", err) + } + return jsonMap +} + +// Try to convert the JSON object to the string value, if the operation failed, returns an empty string. +func JsonToString(jsonObj interface{}) string { + if jsonObj == nil { + return "" + } + jsonStr, err := json.Marshal(jsonObj) + if err != nil { + log.Printf("[ERROR] Unable to convert the JSON object to string: %s", err) + } + return string(jsonStr) +} diff --git a/huaweicloud/utils/type_convert_test.go b/huaweicloud/utils/type_convert_test.go new file mode 100644 index 0000000000..ecd9a001a9 --- /dev/null +++ b/huaweicloud/utils/type_convert_test.go @@ -0,0 +1,88 @@ +package utils_test + +import ( + "fmt" + "reflect" + "testing" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +func TestTypeConvertFunc_StringToJson(t *testing.T) { + var ( + emptyInput = "{}" + correctInput = "{\"foo\":\"bar\"}" + incorrectInput = `func() { + fmt.Println("Hello, this is a function!") + }` + emptyInputExpected = make(map[string]interface{}) + correctInputExpected = map[string]interface{}{ + "foo": "bar", + } + ) + + testOutput := utils.StringToJson(emptyInput) + if !reflect.DeepEqual(testOutput, emptyInputExpected) { + t.Fatalf("The processing result of the StringToJson method is not as expected, want %s, but got %s", + utils.Green(emptyInputExpected), utils.Yellow(testOutput)) + } + + testOutput = utils.StringToJson(correctInput) + if !reflect.DeepEqual(testOutput, correctInputExpected) { + t.Fatalf("The processing result of the StringToJson method is not as expected, want %s, but got %s", + utils.Green(correctInputExpected), utils.Yellow(testOutput)) + } + + testOutput = utils.StringToJson(incorrectInput) + if !reflect.DeepEqual(testOutput, make(map[string]interface{})) { + t.Fatalf("The processing result of the StringToJson method is not as expected, want \"\", but got %s", + utils.Yellow(testOutput)) + } + + t.Logf("All processing results of the JsonToString method meets expectation") +} + +func TestTypeConvertFunc_JsonToString(t *testing.T) { + type Test struct { + Foo string `json:"foo,omitempty"` + } + + var ( + emptyInput = Test{} + correctInput = Test{ + Foo: "bar", + } + emptyInputExpected = "{}" + correctInputExpected = "{\"foo\":\"bar\"}" + // Function is an unsupported type for JsonToString() function input and an error will be returned. + functionInput = func() { + fmt.Println("Hello, this is a function!") + } + ) + + testOutput := utils.JsonToString(emptyInput) + if !reflect.DeepEqual(testOutput, emptyInputExpected) { + t.Fatalf("The processing result of the JsonToString method is not as expected, want %s, but got %s", + utils.Green(emptyInputExpected), utils.Yellow(testOutput)) + } + + testOutput = utils.JsonToString(correctInput) + if !reflect.DeepEqual(testOutput, correctInputExpected) { + t.Fatalf("The processing result of the JsonToString method is not as expected, want %s, but got %s", + utils.Green(correctInputExpected), utils.Yellow(testOutput)) + } + + testOutput = utils.JsonToString(functionInput) + if !reflect.DeepEqual(testOutput, "") { + t.Fatalf("The processing result of the JsonToString method is not as expected, want \"\", but got %s", + utils.Yellow(testOutput)) + } + + testOutput = utils.JsonToString(nil) + if !reflect.DeepEqual(testOutput, "") { + t.Fatalf("The processing result of the JsonToString method is not as expected, want \"\", but got %s", + utils.Yellow(testOutput)) + } + + t.Logf("All processing results of the JsonToString method meets expectation") +}