From 173a9664146974b642755a2bacba776ea1c51e2d Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Wed, 30 Oct 2024 10:26:33 +0800 Subject: [PATCH] feat(cce): add new resource autopilot cluster --- docs/resources/cce_autopilot_cluster.md | 304 ++++++ huaweicloud/provider.go | 3 + ..._huaweicloud_cce_autopilot_cluster_test.go | 116 +++ ...ource_huaweicloud_cce_autopilot_cluster.go | 927 ++++++++++++++++++ 4 files changed, 1350 insertions(+) create mode 100644 docs/resources/cce_autopilot_cluster.md create mode 100644 huaweicloud/services/acceptance/cceautopilot/resource_huaweicloud_cce_autopilot_cluster_test.go create mode 100644 huaweicloud/services/cceautopilot/resource_huaweicloud_cce_autopilot_cluster.go diff --git a/docs/resources/cce_autopilot_cluster.md b/docs/resources/cce_autopilot_cluster.md new file mode 100644 index 0000000000..7ec60ec9b3 --- /dev/null +++ b/docs/resources/cce_autopilot_cluster.md @@ -0,0 +1,304 @@ +--- +subcategory: "Cloud Container Engine Autopilot (CCE Autopilot)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_cce_autopilot_cluster" +description: |- + Manages a CCE Autopilot cluster resource within huaweicloud. +--- + +# huaweicloud_cce_autopilot_cluster + +Manages a CCE Autopilot cluster resource within huaweicloud. + +## Example Usage + +### Basic Usage + +```hcl +resource "huaweicloud_vpc" "myvpc" { + name = "vpc" + cidr = "192.168.0.0/16" +} + +resource "huaweicloud_vpc_subnet" "mysubnet" { + name = "subnet" + cidr = "192.168.0.0/16" + gateway_ip = "192.168.0.1" + vpc_id = huaweicloud_vpc.myvpc.id +} + +resource "huaweicloud_cce_autopilot_cluster" "mycluster" { + name = "cluster" + flavor = "cce.autopilot.cluster" + description = "created by terraform" + + host_network { + vpc = huaweicloud_vpc.myvpc.id + subnet = huaweicloud_vpc_subnet.mysubnet.id + } + + container_network { + mode = "eni" + } + + eni_network { + subnets { + subnet_id = huaweicloud_vpc_subnet.mysubnet.ipv4_subnet_id + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region in which to create the CCE autopilot cluster resource. + If omitted, the provider-level region will be used. Changing this parameter will create a new cluster resource. + +* `name` - (Required, String, NonUpdatable) Specifies the cluster name. Enter 4 to 128 characters starting with a lowercase + letter and not ending with a hyphen (-). Only lowercase letters, digits, and hyphens (-) are allowed. + +* `flavor` - (Required, String, NonUpdatable) Specifies the cluster flavor. Only **cce.autopilot.cluster** is supported. + +* `host_network` - (Required, List, NonUpdatable) Specifies the host network of the cluster. + The [host_network](#autopilot_cluster_host_networks) structure is documented below. + +* `container_network` - (Required, List, NonUpdatable) Specifies the container network of the cluster. + The [container_network](#autopilot_cluster_container_network) structure is documented below. + +* `alias` - (Optional, String, NonUpdatable) Specifies the alias of the cluster. Enter 4 to 128 characters starting + with a lowercase letter and not ending with a hyphen (-). Only lowercase letters, digits, and hyphens (-) are allowed. + If not specified, the alias is the same as the cluster name. + +* `annotations` - (Optional, Map, NonUpdatable) Specifies the cluster annotations in the format of key-value pairs. + +* `category` - (Optional, String, NonUpdatable) Specifies the cluster type. Only **Turbo** is supported. + +* `type` - (Optional, String, NonUpdatable) Specifies the master node architecture. The value can be: + + **VirtualMachine**: Indicates the master node is an x86 server. + +* `description` - (Optional, String, NonUpdatable) Specifies the description of the cluster. + +* `version` - (Optional, String, NonUpdatable) Specifies the version of the cluster. + If not specified, a cluster of the latest version will be created. + +* `custom_san` - (Optional, List, NonUpdatable) Specifies the custom SAN field in the API server certificate of the cluster. + +* `enable_snat` - (Optional, Bool, NonUpdatable) Specifies whether SNAT is configured for the cluster. + After this function is enabled, the cluster can access the Internet through a NAT gateway. + By default, the existing NAT gateway in the selected VPC is used. Otherwise, + the system automatically creates a NAT gateway of the default specifications, + binds an EIP to the NAT gateway, and configures SNAT rules. + +* `enable_swr_image_access` - (Optional, Bool, NonUpdatable) Specifies whether the cluster is interconnected with SWR. + To ensure that your cluster nodes can pull images from SWR, the existing SWR and OBS endpoints in the selected + VPC are used by default. If not, new SWR and OBS endpoints will be automatically created. + +* `enable_autopilot` - (Optional, Bool, NonUpdatable) Specifies whether the cluster is an Autopilot cluster, + defaults to **true**. + +* `ipv6_enable` - (Optional, Bool, NonUpdatable) Specifies whether the cluster uses the IPv6 mode. + +* `eni_network` - (Optional, List, NonUpdatable) Specifies the ENI network of the cluster. + The [eni_network](#autopilot_cluster_eni_network) structure is documented below. + +* `service_network` - (Optional, List, NonUpdatable) Specifies the service network of the cluster. + The [service_network](#autopilot_cluster_service_network) structure is documented below. + +* `authentication` - (Optional, List, NonUpdatable) Specifies the configurations of the cluster authentication mode. + The [authentication](#autopilot_cluster_authentication) structure is documented below. + +* `tags` - (Optional, Map, NonUpdatable) Specifies the cluster tags in the format of key-value pairs. + +* `kube_proxy_mode` - (Optional, String, NonUpdatable) Specifies the kube proxy mode of the cluster. + The value can be: **iptables**. + +* `extend_param` - (Optional, List, NonUpdatable) Specifies the extend param of the cluster. + The [extend_param](#autopilot_cluster_extend_param) structure is documented below. + +* `configurations_override` - (Optional, List, NonUpdatable) Specifies the this parameter to override + the default component configurations in the cluster. + The [configurations_override](#autopilot_cluster_configurations_override) structure is documented below. + +* `deletion_protection` - (Optional, Bool, NonUpdatable) Specifies whether to enable deletion protection for the cluster. + +* `delete_efs` - (Optional, String) Specifies whether to delete the SFS Turbo volume. + The value can be: + + **true** or **block**: The system starts to delete the object. If the deletion fails, subsequent processes are blocked. + + + **try**: The system starts to delete the object. If the deletion fails, no deletion retry is performed, + and subsequent processes will proceed. + + + **false** or **skip**: The deletion is skipped. This is the default option. + +* `delete_eni` - (Optional, String) Specifies whether to delete the ENI port. + The value can be: + + **true** or **block**: The system starts to delete the object. If the deletion fails, subsequent processes are blocked. + This is the default option. + + + **try**: The system starts to delete the object. If the deletion fails, no deletion retry is performed, + and subsequent processes will proceed. + + + **false** or **skip**: The deletion is skipped. + +* `delete_net` - (Optional, String) Specifies whether to delete the cluster service or ingress resources, + such as a load balancer. The value can be: + + **true** or **block**: The system starts to delete the object. If the deletion fails, subsequent processes are blocked. + This is the default option. + + + **try**: The system starts to delete the object. If the deletion fails, no deletion retry is performed, + and subsequent processes will proceed. + + + **false** or **skip**: The deletion is skipped. + +* `delete_obs` - (Optional, String) Specifies whether to delete the OBS volume. + The value can be: + + **true** or **block**: The system starts to delete the object. If the deletion fails, subsequent processes are blocked. + + + **try**: The system starts to delete the object. If the deletion fails, no deletion retry is performed, + and subsequent processes will proceed. + + + **false** or **skip**: The deletion is skipped. This is the default option. + +* `delete_sfs30` - (Optional, String) Specifies whether to delete the SFS 3.0 volume. + The value can be: + + **true** or **block**: The system starts to delete the object. If the deletion fails, subsequent processes are blocked. + + + **try**: The system starts to delete the object. If the deletion fails, no deletion retry is performed, + and subsequent processes will proceed. + + + **false** or **skip**: The deletion is skipped. This is the default option. + +* `lts_reclaim_policy` - (Optional, String) Specifies whether to delete the LTS resource, such as a log group or + a log stream. The value can be: + + **Delete_Log_Group**: The system starts to delete a log group. If the deletion fails, no deletion retry is performed, + and subsequent processes will proceed. + + + **Delete_Master_Log_Stream**: The system starts to delete a master log stream. If the deletion fails, + no deletion retry is performed, and subsequent processes will proceed. This is the default option. + + + **Retain**: The deletion is skipped. + + +The `host_network` block supports: + +* `vpc` - (Required, String, NonUpdatable) Specifies the ID of the VPC used to create a master node. + +* `subnet` - (Required, String, NonUpdatable) Specifies ID of the subnet used to create a master node. + + +The `container_network` block supports: + +* `mode` - (Required, String, NonUpdatable) Specifies the container network type. The value can be: **eni**. + + +The `eni_network` block supports: + +* `subnets` - (Required, List, NonUpdatable) Specifies the list of ENI subnets. + The [subnets](#autopilot_cluster_eni_network_subnets) structure is documented below. + + +The `subnets` block supports: + +* `subnet_id` - (Required, String, NonUpdatable) Specifies the IPv4 subnet ID of the subnet used to create control + nodes and containers. + + +The `service_network` block supports: + +* `ipv4_cidr` - (Optional, String, NonUpdatable) Specifies the IPv4 CIDR of the service network. + If not specified, the default value 10.247.0.0/16 will be used. + + +The `authentication` block supports: + +* `mode` - (Optional, String, NonUpdatable) Specifies the cluster authentication mode. + The default value is **rbac**. + + +The `extend_param` block supports: + +* `enterprise_project_id` - (Optional, String, NonUpdatable) Specifies the ID of the enterprise project to which the + cluster belongs. + + +The `configurations_override` block supports: + +* `name` - (Optional, String, NonUpdatable) Specifies the component name. + +* `configurations` - (Optional, List, NonUpdatable) Specifies the component configuration items. + The [configurations](#autopilot_cluster_configurations_override_configurations) structure is documented below. + + +The `configurations` block supports: + +* `name` - (Optional, String, NonUpdatable) Specifies the component configuration item name. + +* `value` - (Optional, String, NonUpdatable) Specifies the component configuration item value. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - ID of the cluster resource. + +* `platform_version` - The cluster platform version. + +* `created_at` - The time when the cluster was created. + +* `updated_at` - The time when the cluster was updated. + +* `az` - The AZ of the cluster. + +* `status` - The status of the cluster. + The [status](#autopilot_cluster_status) structure is documented below. + + +The `status` block supports: + +* `phase` - The phase of the cluster. + +* `endpoints` - The access address of kube-apiserver in the cluster. + The [endpoints](#autopilot_cluster_status_endpoints) structure is documented below. + + +The `endpoints` block supports: + +* `url` - The phase of the cluster. + +* `type` - The access address of kube-apiserver in the cluster. + +## Timeouts + +This resource provides the following timeouts configuration options: + +* `create` - Default is 30 minutes. +* `delete` - Default is 30 minutes. + +## Import + +The autopilot cluster can be imported using the cluster ID, e.g. + +```bash + $ terraform import huaweicloud_cce_autopilot_cluster.mycluster +``` + +Note that the imported state may not be identical to your resource definition, due to some attributes missing from the +API response, security or some other reason. The missing attributes include: +`delete_efs`, `delete_eni`, `delete_net`, `delete_obs`, `delete_sfs30` and `lts_reclaim_policy`. It is generally +recommended running `terraform plan` after importing a cluster. You can then decide if changes should be applied to +the cluster, or the resource definition should be updated to align with the cluster. Also you can ignore changes as +below. + +```hcl +resource "huaweicloud_cce_autopilot_cluster" "mycluster" { + ... + + lifecycle { + ignore_changes = [ + delete_efs, delete_obs, + ] + } +} +``` diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index 3f8da57e65..5959a7632c 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -30,6 +30,7 @@ import ( "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/cbr" "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/cc" "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/cce" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/cceautopilot" "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/cci" "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/ccm" "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/cdm" @@ -1300,6 +1301,8 @@ func Provider() *schema.Provider { "huaweicloud_cc_global_connection_bandwidth": cc.ResourceGlobalConnectionBandwidth(), "huaweicloud_cc_global_connection_bandwidth_associate": cc.ResourceGlobalConnectionBandwidthAssociate(), + "huaweicloud_cce_autopilot_cluster": cceautopilot.ResourceAutopilotCluster(), + "huaweicloud_cce_cluster": cce.ResourceCluster(), "huaweicloud_cce_cluster_log_config": cce.ResourceClusterLogConfig(), "huaweicloud_cce_cluster_upgrade": cce.ResourceClusterUpgrade(), diff --git a/huaweicloud/services/acceptance/cceautopilot/resource_huaweicloud_cce_autopilot_cluster_test.go b/huaweicloud/services/acceptance/cceautopilot/resource_huaweicloud_cce_autopilot_cluster_test.go new file mode 100644 index 0000000000..25652682cb --- /dev/null +++ b/huaweicloud/services/acceptance/cceautopilot/resource_huaweicloud_cce_autopilot_cluster_test.go @@ -0,0 +1,116 @@ +package cceautopilot + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/chnsz/golangsdk" + + "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/acceptance/common" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +func getAutopilotClusterFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) { + region := acceptance.HW_REGION_NAME + + var ( + getClusterHttpUrl = "autopilot/v3/projects/{project_id}/clusters/{cluster_id}" + getClusterProduct = "cce" + ) + getClusterClient, err := cfg.NewServiceClient(getClusterProduct, region) + if err != nil { + return nil, fmt.Errorf("error creating CCE Client: %s", err) + } + + getClusterPath := getClusterClient.Endpoint + getClusterHttpUrl + getClusterPath = strings.ReplaceAll(getClusterPath, "{project_id}", getClusterClient.ProjectID) + getClusterPath = strings.ReplaceAll(getClusterPath, "{cluster_id}", state.Primary.ID) + + getClusterOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + + getClusterResp, err := getClusterClient.Request("GET", getClusterPath, &getClusterOpt) + if err != nil { + return nil, fmt.Errorf("error retrieving CCE autopolit cluster: %s", err) + } + + return utils.FlattenResponse(getClusterResp) +} + +func TestAccAutopilotCluster_basic(t *testing.T) { + var ( + cluster interface{} + resourceName = "huaweicloud_cce_autopilot_cluster.test" + rName = acceptance.RandomAccResourceNameWithDash() + + rc = acceptance.InitResourceCheck( + resourceName, + &cluster, + getAutopilotClusterFunc, + ) + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccCluster_basic(rName), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "flavor", "cce.autopilot.cluster"), + resource.TestCheckResourceAttr(resourceName, "description", "created by terraform"), + resource.TestCheckResourceAttr(resourceName, "container_network.0.mode", "eni"), + resource.TestCheckResourceAttrPair(resourceName, "host_network.0.vpc", "huaweicloud_vpc.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "host_network.0.subnet", "huaweicloud_vpc_subnet.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "eni_network.0.subnets.0.subnet_id", + "huaweicloud_vpc_subnet.test", "ipv4_subnet_id"), + resource.TestCheckResourceAttr(resourceName, "status.0.phase", "Available"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "updated_at"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCluster_basic(rName string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_cce_autopilot_cluster" "test" { + name = "%[2]s" + flavor = "cce.autopilot.cluster" + description = "created by terraform" + + host_network { + vpc = huaweicloud_vpc.test.id + subnet = huaweicloud_vpc_subnet.test.id + } + + container_network { + mode = "eni" + } + + eni_network { + subnets { + subnet_id = huaweicloud_vpc_subnet.test.ipv4_subnet_id + } + } +} +`, common.TestVpc(rName), rName) +} diff --git a/huaweicloud/services/cceautopilot/resource_huaweicloud_cce_autopilot_cluster.go b/huaweicloud/services/cceautopilot/resource_huaweicloud_cce_autopilot_cluster.go new file mode 100644 index 0000000000..39cf8e9dae --- /dev/null +++ b/huaweicloud/services/cceautopilot/resource_huaweicloud_cce_autopilot_cluster.go @@ -0,0 +1,927 @@ +package cceautopilot + +import ( + "context" + "fmt" + "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/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 autopilotClusterNonUpdatableParams = []string{ + "name", "flavor", + "host_network", "host_network.*.vpc", "host_network.*.subnet", + "container_network", "container_network.*.mode", + "alias", "annotations", "category", "type", "version", "description", "custom_san", "enable_snat", + "enable_swr_image_access", "enable_autopilot", "ipv6_enable", + "eni_network", "eni_network.*.subnets", "eni_network.*.subnets.*.subnet_id", + "service_network", "service_network.*.ipv4_cidr", + "authentication", "authentication.*.mode", + "tags", "kube_proxy_mode", + "extend_param", "extend_param.*.enterprise_project_id", + "configurations_override", "configurations_override.*.name", "configurations_override.*.configurations", + "configurations_override.*.configurations.*.name", "configurations_override.*.configurations.*.value", + "deletion_protection", +} + +// @API CCE POST /autopilot/v3/projects/{project_id}/clusters +// @API CCE GET /autopilot/v3/projects/{project_id}/jobs/{job_id} +// @API CCE GET /autopilot/v3/projects/{project_id}/clusters/{cluster_id} +// @API CCE DELETE /autopilot/v3/projects/{project_id}/clusters/{cluster_id} +func ResourceAutopilotCluster() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAutopilotClusterCreate, + ReadContext: resourceAutopilotClusterRead, + UpdateContext: resourceAutopilotClusterUpdate, + DeleteContext: resourceAutopilotClusterDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + CustomizeDiff: config.FlexibleForceNew(autopilotClusterNonUpdatableParams), + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "flavor": { + Type: schema.TypeString, + Required: true, + }, + "host_network": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "vpc": { + Type: schema.TypeString, + Required: true, + }, + "subnet": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "container_network": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "mode": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "alias": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "annotations": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "category": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + DiffSuppressFunc: utils.SuppressVersionDiffs, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "custom_san": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "enable_snat": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "enable_swr_image_access": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "enable_autopilot": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "ipv6_enable": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "eni_network": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subnets": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subnet_id": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "service_network": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ipv4_cidr": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "authentication": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "mode": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "tags": common.TagsSchema(), + "kube_proxy_mode": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "extend_param": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enterprise_project_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "configurations_override": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "configurations": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "deletion_protection": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "delete_efs": { + Type: schema.TypeString, + Optional: true, + }, + "delete_eni": { + Type: schema.TypeString, + Optional: true, + }, + "delete_net": { + Type: schema.TypeString, + Optional: true, + }, + "delete_obs": { + Type: schema.TypeString, + Optional: true, + }, + "delete_sfs30": { + Type: schema.TypeString, + Optional: true, + }, + "lts_reclaim_policy": { + Type: schema.TypeString, + Optional: true, + }, + "platform_version": { + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + }, + "az": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "phase": { + Type: schema.TypeString, + Computed: true, + }, + "endpoints": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "url": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func buildClusterBodyParams(d *schema.ResourceData) map[string]interface{} { + bodyParams := map[string]interface{}{ + "kind": "Cluster", + "apiVersion": "v3", + "metadata": buildMetadataBodyParams(d), + "spec": buildSpecBodyParams(d), + } + + return bodyParams +} + +func buildMetadataBodyParams(d *schema.ResourceData) map[string]interface{} { + bodyParams := map[string]interface{}{ + "name": d.Get("name"), + "alias": utils.ValueIgnoreEmpty(d.Get("alias")), + "annotations": utils.ValueIgnoreEmpty(d.Get("annotations")), + } + + return bodyParams +} + +func buildSpecBodyParams(d *schema.ResourceData) map[string]interface{} { + bodyParams := map[string]interface{}{ + "category": utils.ValueIgnoreEmpty(d.Get("category")), + "type": utils.ValueIgnoreEmpty(d.Get("type")), + "flavor": d.Get("flavor"), + "description": utils.ValueIgnoreEmpty(d.Get("description")), + "customSan": utils.ValueIgnoreEmpty(d.Get("custom_san")), + "enableSnat": d.Get("enable_snat"), + "enableSWRImageAccess": d.Get("enable_swr_image_access"), + "enableAutopilot": d.Get("enable_autopilot"), + "ipv6enable": d.Get("ipv6_enable"), + "hostNetwork": buildHostNetworkBodyParams(d), + "containerNetwork": buildContainerNetworkBodyParams(d), + "eniNetwork": buildEniNetworkBodyParams(d), + "serviceNetwork": buildServiceNetworkBodyParams(d), + "authentication": buildAuthenticationBodyParams(d), + "clusterTags": utils.ExpandResourceTagsMap(d.Get("tags").(map[string]interface{})), + "kubeProxyMode": utils.ValueIgnoreEmpty(d.Get("kube_proxy_mode")), + "extendParam": buildExtendParamBodyParams(d), + "configurationsOverride": buildConfigurationsOverrideBodyParams(d), + "deleteProtection": d.Get("delete_protection"), + } + + return bodyParams +} + +func buildHostNetworkBodyParams(d *schema.ResourceData) map[string]interface{} { + hostNetwork := d.Get("host_network").([]interface{}) + if len(hostNetwork) == 0 { + return nil + } + + bodyParams := map[string]interface{}{ + "vpc": utils.PathSearch("vpc", hostNetwork[0], nil), + "subnet": utils.PathSearch("subnet", hostNetwork[0], nil), + } + + return bodyParams +} + +func buildContainerNetworkBodyParams(d *schema.ResourceData) map[string]interface{} { + containerNetwork := d.Get("container_network").([]interface{}) + if len(containerNetwork) == 0 { + return nil + } + + bodyParams := map[string]interface{}{ + "mode": utils.PathSearch("mode", containerNetwork[0], nil), + } + + return bodyParams +} + +func buildEniNetworkBodyParams(d *schema.ResourceData) map[string]interface{} { + eniNetwork := d.Get("eni_network").([]interface{}) + if len(eniNetwork) == 0 { + return nil + } + + subnetsRaw := utils.PathSearch("subnets", eniNetwork[0], []interface{}{}).([]interface{}) + subnets := make([]map[string]interface{}, len(subnetsRaw)) + for i, v := range subnetsRaw { + subnets[i] = map[string]interface{}{ + "subnetID": utils.PathSearch("subnet_id", v, nil), + } + } + + bodyParams := map[string]interface{}{ + "subnets": subnets, + } + + return bodyParams +} + +func buildServiceNetworkBodyParams(d *schema.ResourceData) map[string]interface{} { + serviceNetwork := d.Get("service_network").([]interface{}) + if len(serviceNetwork) == 0 { + return nil + } + + bodyParams := map[string]interface{}{ + "IPv4CIDR": utils.PathSearch("ipv4_cidr", serviceNetwork[0], nil), + } + + return bodyParams +} + +func buildAuthenticationBodyParams(d *schema.ResourceData) map[string]interface{} { + authentication := d.Get("authentication").([]interface{}) + if len(authentication) == 0 { + return nil + } + + bodyParams := map[string]interface{}{ + "mode": utils.PathSearch("mode", authentication[0], nil), + } + + return bodyParams +} + +func buildExtendParamBodyParams(d *schema.ResourceData) map[string]interface{} { + extendParam := d.Get("extend_param").([]interface{}) + if len(extendParam) == 0 { + return nil + } + + bodyParams := map[string]interface{}{ + "enterpriseProjectId": utils.PathSearch("enterprise_project_id", extendParam[0], nil), + } + + return bodyParams +} + +func buildConfigurationsOverrideBodyParams(d *schema.ResourceData) []map[string]interface{} { + configurationsOverrideRaw := d.Get("configurations_override").([]interface{}) + if len(configurationsOverrideRaw) == 0 { + return nil + } + + bodyParams := make([]map[string]interface{}, len(configurationsOverrideRaw)) + + for i, v := range configurationsOverrideRaw { + bodyParams[i] = map[string]interface{}{ + "name": utils.PathSearch("name", v, nil), + "configurations": buildConfigurationsBodyParams(v), + } + } + + return bodyParams +} + +func buildConfigurationsBodyParams(configurationsOverride interface{}) []map[string]interface{} { + configurationsRaw := utils.PathSearch("configurations", configurationsOverride, []interface{}{}).([]interface{}) + if len(configurationsRaw) == 0 { + return nil + } + + bodyParams := make([]map[string]interface{}, len(configurationsRaw)) + + for i, v := range configurationsRaw { + bodyParams[i] = map[string]interface{}{ + "name": utils.PathSearch("name", v, nil), + "value": utils.PathSearch("value", v, nil), + } + } + + return bodyParams +} + +func resourceAutopilotClusterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + + var ( + createClusterHttpUrl = "autopilot/v3/projects/{project_id}/clusters" + createClusterProduct = "cce" + ) + createClusterClient, err := cfg.NewServiceClient(createClusterProduct, region) + if err != nil { + return diag.Errorf("error creating CCE Client: %s", err) + } + + createClusterPath := createClusterClient.Endpoint + createClusterHttpUrl + createClusterPath = strings.ReplaceAll(createClusterPath, "{project_id}", createClusterClient.ProjectID) + + createClusterOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + + createOpts := buildClusterBodyParams(d) + createClusterOpt.JSONBody = utils.RemoveNil(createOpts) + createClusterResp, err := createClusterClient.Request("POST", createClusterPath, &createClusterOpt) + if err != nil { + return diag.Errorf("error creating CCE autopolit cluster: %s", err) + } + + createClusterRespBody, err := utils.FlattenResponse(createClusterResp) + if err != nil { + return diag.FromErr(err) + } + + id := utils.PathSearch("metadata.uid", createClusterRespBody, "").(string) + if id == "" { + return diag.Errorf("error creating CCE autopilot cluster: ID is not found in API response") + } + d.SetId(id) + + jobID := utils.PathSearch("status.jobID", createClusterRespBody, "").(string) + if jobID == "" { + return diag.Errorf("error creating CCE autopilot cluster: jobID is not found in API response") + } + + err = clusterJobWaitingForStateCompleted(ctx, d, meta, d.Timeout(schema.TimeoutCreate), jobID) + if err != nil { + return diag.Errorf("error waiting for creating CCE autopilot cluster (%s) to complete: %s", id, err) + } + + return resourceAutopilotClusterRead(ctx, d, meta) +} + +func resourceAutopilotClusterRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + + var ( + getClusterHttpUrl = "autopilot/v3/projects/{project_id}/clusters/{cluster_id}" + getClusterProduct = "cce" + ) + getClusterClient, err := cfg.NewServiceClient(getClusterProduct, region) + if err != nil { + return diag.Errorf("error creating CCE Client: %s", err) + } + + getClusterPath := getClusterClient.Endpoint + getClusterHttpUrl + getClusterPath = strings.ReplaceAll(getClusterPath, "{project_id}", getClusterClient.ProjectID) + getClusterPath = strings.ReplaceAll(getClusterPath, "{cluster_id}", d.Id()) + + getClusterOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + + getClusterResp, err := getClusterClient.Request("GET", getClusterPath, &getClusterOpt) + if err != nil { + return common.CheckDeletedDiag(d, err, "error retrieving CCE autopolit cluster") + } + + getClusterRespBody, err := utils.FlattenResponse(getClusterResp) + if err != nil { + return diag.FromErr(err) + } + + mErr := multierror.Append(nil, + d.Set("region", cfg.GetRegion(d)), + d.Set("name", utils.PathSearch("metadata.name", getClusterRespBody, nil)), + d.Set("alias", utils.PathSearch("metadata.alias", getClusterRespBody, nil)), + d.Set("annotations", utils.PathSearch("metadata.annotations", getClusterRespBody, nil)), + d.Set("category", utils.PathSearch("spec.category", getClusterRespBody, nil)), + d.Set("type", utils.PathSearch("spec.type", getClusterRespBody, nil)), + d.Set("flavor", utils.PathSearch("spec.flavor", getClusterRespBody, nil)), + d.Set("version", utils.PathSearch("spec.version", getClusterRespBody, nil)), + d.Set("description", utils.PathSearch("spec.description", getClusterRespBody, nil)), + d.Set("custom_san", utils.PathSearch("spec.customSan", getClusterRespBody, nil)), + d.Set("enable_snat", utils.PathSearch("spec.enableSnat", getClusterRespBody, nil)), + d.Set("enable_swr_image_access", utils.PathSearch("spec.enableSWRImageAccess", getClusterRespBody, nil)), + d.Set("enable_autopilot", utils.PathSearch("spec.enableAutopilot", getClusterRespBody, nil)), + d.Set("ipv6_enable", utils.PathSearch("spec.ipv6enable", getClusterRespBody, nil)), + d.Set("host_network", flattenHostNetwork(getClusterRespBody)), + d.Set("container_network", flattenContainerNetwork(getClusterRespBody)), + d.Set("eni_network", flattenEniNetwork(getClusterRespBody)), + d.Set("service_network", flattenServiceNetwork(getClusterRespBody)), + d.Set("authentication", flattenAuthentication(getClusterRespBody)), + d.Set("tags", utils.FlattenTagsToMap(utils.PathSearch("spec.clusterTags", getClusterRespBody, nil))), + d.Set("kube_proxy_mode", utils.PathSearch("spec.kubeProxyMode", getClusterRespBody, nil)), + d.Set("az", utils.PathSearch("spec.az", getClusterRespBody, nil)), + d.Set("extend_param", flattenExtendParam(getClusterRespBody)), + d.Set("configurations_override", flattenConfigurationsOverride(getClusterRespBody)), + d.Set("deletion_protection", utils.PathSearch("spec.deletionProtection", getClusterRespBody, nil)), + d.Set("platform_version", utils.PathSearch("spec.platformVersion", getClusterRespBody, nil)), + d.Set("created_at", utils.PathSearch("metadata.creationTimestamp", getClusterRespBody, nil)), + d.Set("updated_at", utils.PathSearch("metadata.updateTimestamp", getClusterRespBody, nil)), + d.Set("status", flattenStatus(getClusterRespBody)), + ) + + return diag.FromErr(mErr.ErrorOrNil()) +} + +func flattenHostNetwork(getClusterRespBody interface{}) []map[string]interface{} { + hostNetwork := utils.PathSearch("spec.hostNetwork", getClusterRespBody, nil) + if hostNetwork == nil { + return nil + } + + res := []map[string]interface{}{ + { + "vpc": utils.PathSearch("vpc", hostNetwork, nil), + "subnet": utils.PathSearch("subnet", hostNetwork, nil), + }, + } + + return res +} + +func flattenContainerNetwork(getClusterRespBody interface{}) []map[string]interface{} { + containerNetwork := utils.PathSearch("spec.containerNetwork", getClusterRespBody, nil) + if containerNetwork == nil { + return nil + } + + res := []map[string]interface{}{ + { + "mode": utils.PathSearch("mode", containerNetwork, nil), + }, + } + + return res +} + +func flattenEniNetwork(getClusterRespBody interface{}) []map[string]interface{} { + eniNetwork := utils.PathSearch("spec.eniNetwork", getClusterRespBody, nil) + if eniNetwork == nil { + return nil + } + + subnetsRaw := utils.PathSearch("subnets", eniNetwork, []interface{}{}).([]interface{}) + subnets := make([]map[string]interface{}, len(subnetsRaw)) + for i, v := range subnetsRaw { + subnets[i] = map[string]interface{}{ + "subnet_id": utils.PathSearch("subnetID", v, nil), + } + } + + res := []map[string]interface{}{ + { + "subnets": subnets, + }, + } + + return res +} + +func flattenServiceNetwork(getClusterRespBody interface{}) []map[string]interface{} { + serviceNetwork := utils.PathSearch("spec.serviceNetwork", getClusterRespBody, nil) + if serviceNetwork == nil { + return nil + } + + res := []map[string]interface{}{ + { + "ipv4_cidr": utils.PathSearch("IPv4CIDR", serviceNetwork, nil), + }, + } + + return res +} + +func flattenAuthentication(getClusterRespBody interface{}) []map[string]interface{} { + authentication := utils.PathSearch("spec.authentication", getClusterRespBody, nil) + if authentication == nil { + return nil + } + + res := []map[string]interface{}{ + { + "mode": utils.PathSearch("mode", authentication, nil), + }, + } + + return res +} + +func flattenExtendParam(getClusterRespBody interface{}) []map[string]interface{} { + extendParam := utils.PathSearch("spec.extendParam", getClusterRespBody, nil) + if extendParam == nil { + return nil + } + + res := []map[string]interface{}{ + { + "enterprise_project_id": utils.PathSearch("enterpriseProjectId", extendParam, nil), + }, + } + + return res +} + +func flattenConfigurationsOverride(getClusterRespBody interface{}) []map[string]interface{} { + configurationsOverrideRaw := utils.PathSearch("spec.configurationsOverride", getClusterRespBody, []interface{}{}).([]interface{}) + if len(configurationsOverrideRaw) == 0 { + return nil + } + + res := make([]map[string]interface{}, len(configurationsOverrideRaw)) + for i, v := range configurationsOverrideRaw { + res[i] = map[string]interface{}{ + "name": utils.PathSearch("name", v, nil), + "configurations": flattenConfigurations(v), + } + } + + return res +} + +func flattenConfigurations(configurationsOverride interface{}) []map[string]interface{} { + configurationsRaw := utils.PathSearch("configurations", configurationsOverride, []interface{}{}).([]interface{}) + if len(configurationsRaw) == 0 { + return nil + } + + res := make([]map[string]interface{}, len(configurationsRaw)) + for i, v := range configurationsRaw { + res[i] = map[string]interface{}{ + "name": utils.PathSearch("name", v, nil), + "value": utils.PathSearch("value", v, nil), + } + } + + return res +} + +func flattenStatus(getClusterRespBody interface{}) []map[string]interface{} { + status := utils.PathSearch("status", getClusterRespBody, nil) + if status == nil { + return nil + } + + endpointsRaw := utils.PathSearch("endpoints", status, []interface{}{}).([]interface{}) + endpoints := make([]map[string]interface{}, len(endpointsRaw)) + for i, v := range endpointsRaw { + endpoints[i] = map[string]interface{}{ + "url": utils.PathSearch("url", v, nil), + "type": utils.PathSearch("type", v, nil), + } + } + + res := []map[string]interface{}{ + { + "endpoints": endpoints, + "phase": utils.PathSearch("phase", status, nil), + }, + } + + return res +} + +func resourceAutopilotClusterUpdate(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil +} + +func resourceAutopilotClusterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + + var ( + deleteClusterHttpUrl = "autopilot/v3/projects/{project_id}/clusters/{cluster_id}" + deleteClusterProduct = "cce" + ) + deleteClusterClient, err := cfg.NewServiceClient(deleteClusterProduct, region) + if err != nil { + return diag.Errorf("error creating CCE Client: %s", err) + } + + deleteClusterPath := deleteClusterClient.Endpoint + deleteClusterHttpUrl + deleteClusterPath = strings.ReplaceAll(deleteClusterPath, "{project_id}", deleteClusterClient.ProjectID) + deleteClusterPath = strings.ReplaceAll(deleteClusterPath, "{cluster_id}", d.Id()) + + deleteClusterOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + + deleteClusteQueryParams := buildDeleteClusteQueryParams(d) + deleteClusterPath += deleteClusteQueryParams + + deleteClusterResp, err := deleteClusterClient.Request("DELETE", deleteClusterPath, &deleteClusterOpt) + if err != nil { + return common.CheckDeletedDiag(d, err, "error deleting CCE autopolit cluster") + } + + deleteClusterRespBody, err := utils.FlattenResponse(deleteClusterResp) + if err != nil { + return diag.FromErr(err) + } + + jobID := utils.PathSearch("status.jobID", deleteClusterRespBody, "").(string) + if jobID == "" { + return diag.Errorf("error deleting CCE autopilot cluster: jobID is not found in API response") + } + + err = clusterJobWaitingForStateCompleted(ctx, d, meta, d.Timeout(schema.TimeoutDelete), jobID) + if err != nil { + return diag.Errorf("error waiting for deleting CCE autopilot cluster (%s) to complete: %s", d.Id(), err) + } + + return nil +} + +func buildDeleteClusteQueryParams(d *schema.ResourceData) string { + res := "" + + if v, ok := d.GetOk("delete_efs"); ok { + res = fmt.Sprintf("%s&delete_efs=%v", res, v) + } + if v, ok := d.GetOk("delete_eni"); ok { + res = fmt.Sprintf("%s&delete_eni=%v", res, v) + } + if v, ok := d.GetOk("delete_net"); ok { + res = fmt.Sprintf("%s&delete_net=%v", res, v) + } + if v, ok := d.GetOk("delete_obs"); ok { + res = fmt.Sprintf("%s&delete_obs=%v", res, v) + } + if v, ok := d.GetOk("delete_sfs30"); ok { + res = fmt.Sprintf("%s&delete_sfs30=%v", res, v) + } + if v, ok := d.GetOk("lts_reclaim_policy"); ok { + res = fmt.Sprintf("%s<s_reclaim_policy=%v", res, v) + } + + if res != "" { + res = "?" + res[1:] + } + + return res +} + +func clusterJobWaitingForStateCompleted(ctx context.Context, d *schema.ResourceData, meta interface{}, t time.Duration, jobID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING"}, + Target: []string{"COMPLETED"}, + Refresh: func() (interface{}, string, error) { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + var ( + clusterJobWaitingHttpUrl = "autopilot/v3/projects/{project_id}/jobs/{job_id}" + clusterJobWaitingProduct = "cce" + ) + clusterJobWaitingClient, err := cfg.NewServiceClient(clusterJobWaitingProduct, region) + if err != nil { + return nil, "ERROR", fmt.Errorf("error creating CCE client: %s", err) + } + + clusterJobWaitingPath := clusterJobWaitingClient.Endpoint + clusterJobWaitingHttpUrl + clusterJobWaitingPath = strings.ReplaceAll(clusterJobWaitingPath, "{project_id}", clusterJobWaitingClient.ProjectID) + clusterJobWaitingPath = strings.ReplaceAll(clusterJobWaitingPath, "{job_id}", jobID) + + clusterJobWaitingOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + clusterJobWaitingResp, err := clusterJobWaitingClient.Request("GET", clusterJobWaitingPath, &clusterJobWaitingOpt) + if err != nil { + return nil, "ERROR", err + } + + clusterJobWaitingRespBody, err := utils.FlattenResponse(clusterJobWaitingResp) + if err != nil { + return nil, "ERROR", err + } + status := utils.PathSearch(`status.phase`, clusterJobWaitingRespBody, nil) + if status == nil { + return nil, "ERROR", fmt.Errorf("error parsing %s from response body", `status.phase`) + } + + targetStatus := []string{ + "Success", + } + if utils.StrSliceContains(targetStatus, status.(string)) { + return clusterJobWaitingRespBody, "COMPLETED", nil + } + + unexpectedStatus := []string{ + "Failed", + } + if utils.StrSliceContains(unexpectedStatus, status.(string)) { + return clusterJobWaitingRespBody, status.(string), nil + } + + return clusterJobWaitingRespBody, "PENDING", nil + }, + Timeout: t, + Delay: 10 * time.Second, + PollInterval: 5 * time.Second, + } + _, err := stateConf.WaitForStateContext(ctx) + return err +}