From 1027e6268003d23266c46afece6c7f04505d81ac Mon Sep 17 00:00:00 2001 From: Lance52259 Date: Mon, 6 Jan 2025 10:13:35 +0800 Subject: [PATCH] feat(cae/app): add new resource to manage applications --- docs/resources/cae_application.md | 57 +++++ huaweicloud/provider.go | 1 + ...source_huaweicloud_cae_application_test.go | 137 +++++++++++ .../resource_huaweicloud_cae_application.go | 223 ++++++++++++++++++ 4 files changed, 418 insertions(+) create mode 100644 docs/resources/cae_application.md create mode 100644 huaweicloud/services/acceptance/cae/resource_huaweicloud_cae_application_test.go create mode 100644 huaweicloud/services/cae/resource_huaweicloud_cae_application.go diff --git a/docs/resources/cae_application.md b/docs/resources/cae_application.md new file mode 100644 index 0000000000..e3b5075163 --- /dev/null +++ b/docs/resources/cae_application.md @@ -0,0 +1,57 @@ +--- +subcategory: "Cloud Application Engine (CAE)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_cae_application" +description: |- + Manages an application resource within HuaweiCloud. +--- + +# huaweicloud_cae_application + +Manages an application resource within HuaweiCloud. + +## Example Usage + +```hcl +variable "environment_id" {} +variable "application_name" {} + +resource "huaweicloud_cae_application" "test" { + environment_id = var.environment_id + name = var.application_name +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region where the application is located. + If omitted, the provider-level region will be used. Changing this creates a new resource. + +* `environment_id` - (Required, String, ForceNew) Specifies the ID of the environment to which the application + belongs. + Changing this creates a new resource. + +* `name` - (Required, String, ForceNew) Specifies the name of the application. + The valid length is limited from `2` to `63`, only lowercase letters, digits and hyphens (-) are allowed. + The name must start with a lowercase letter and end with a lowercase letter or a digit. + Changing this creates a new resource. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID. + +* `created_at` - The creation time of the application, in RFC3339 format. + +* `updated_at` - The latest update time of the application, in RFC3339 format. + +## Import + +The application can be imported using `environment_id` and `id`, separated by a slash (/), e.g. + +```bash +$ terraform import huaweicloud_cae_application.test / +``` diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index 2554fb81a6..431c2f0eb6 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1420,6 +1420,7 @@ func Provider() *schema.Provider { "huaweicloud_bms_instance": bms.ResourceBmsInstance(), "huaweicloud_bcs_instance": bcs.ResourceInstance(), + "huaweicloud_cae_application": cae.ResourceApplication(), "huaweicloud_cae_component": cae.ResourceComponent(), "huaweicloud_cae_component_configurations": cae.ResourceComponentConfigurations(), "huaweicloud_cae_component_deployment": cae.ResourceComponentDeployment(), diff --git a/huaweicloud/services/acceptance/cae/resource_huaweicloud_cae_application_test.go b/huaweicloud/services/acceptance/cae/resource_huaweicloud_cae_application_test.go new file mode 100644 index 0000000000..5d7299df25 --- /dev/null +++ b/huaweicloud/services/acceptance/cae/resource_huaweicloud_cae_application_test.go @@ -0,0 +1,137 @@ +package cae + +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/cae" +) + +func getApplicationFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) { + client, err := cfg.NewServiceClient("cae", acceptance.HW_REGION_NAME) + if err != nil { + return nil, fmt.Errorf("error creating CAE client: %s", err) + } + + envId := state.Primary.Attributes["environment_id"] + return cae.GetApplicationById(client, envId, state.Primary.ID) +} + +func TestAccApplication_basic(t *testing.T) { + var ( + obj interface{} + + resourceName = "huaweicloud_cae_application.test" + rc = acceptance.InitResourceCheck(resourceName, &obj, getApplicationFunc) + + invalidName = "-tf-test-invalid-name" + name = acceptance.RandomAccResourceNameWithDash() + baseConfig = testAccApplication_base(name) + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccApplication_basic(baseConfig, invalidName), + ExpectError: regexp.MustCompile(`CAE.01500214`), + }, + { + Config: testAccApplication_basic(baseConfig, name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(resourceName, "name", name), + 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}))$`)), + 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: testAccApplicationImportStateFunc(resourceName), + }, + }, + }) +} + +func testAccApplicationImportStateFunc(rsName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[rsName] + if !ok { + return "", fmt.Errorf("resource (%s) not found: %s", rsName, rs) + } + + var ( + environmentId = rs.Primary.Attributes["environment_id"] + applicationId = rs.Primary.ID + ) + if environmentId == "" || applicationId == "" { + return "", fmt.Errorf("some import IDs are missing, want '/', but got '%s/%s'", + environmentId, applicationId) + } + + return fmt.Sprintf("%s/%s", environmentId, applicationId), nil + } +} + +func testAccApplication_base(name string) string { + return fmt.Sprintf(` +resource "huaweicloud_vpc" "test" { + name = "%[1]s" + cidr = "192.168.0.0/16" +} + +resource "huaweicloud_vpc_subnet" "test" { + vpc_id = huaweicloud_vpc.test.id + name = "%[1]s" + cidr = cidrsubnet(huaweicloud_vpc.test.cidr, 4, 1) + gateway_ip = cidrhost(cidrsubnet(huaweicloud_vpc.test.cidr, 4, 1), 1) +} + +resource "huaweicloud_networking_secgroup" "test" { + name = "%[1]s" + delete_default_rules = true +} + +resource "huaweicloud_swr_organization" "test" { + name = "%[1]s" +} + +resource "huaweicloud_cae_environment" "test" { + name = "%[1]s" + + annotations = { + type = "exclusive" + vpc_id = huaweicloud_vpc.test.id + subnet_id = huaweicloud_vpc_subnet.test.id + security_group_id = huaweicloud_networking_secgroup.test.id + group_name = huaweicloud_swr_organization.test.name + } + + // To avoid k8s container deploy failed. + max_retries = 1 +} +`, name) +} + +func testAccApplication_basic(baseConfig, name string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_cae_application" "test" { + environment_id = huaweicloud_cae_environment.test.id + name = "%[2]s" +} +`, baseConfig, name) +} diff --git a/huaweicloud/services/cae/resource_huaweicloud_cae_application.go b/huaweicloud/services/cae/resource_huaweicloud_cae_application.go new file mode 100644 index 0000000000..d9001753e9 --- /dev/null +++ b/huaweicloud/services/cae/resource_huaweicloud_cae_application.go @@ -0,0 +1,223 @@ +package cae + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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 appResourceNotFoundCodes = []string{ + "CAE.01500003", // Application is not exist. +} + +// @API CAE POST /v1/{project_id}/cae/applications +// @API CAE GET /v1/{project_id}/cae/applications/{application_id} +// @API CAE DELETE /v1/{project_id}/cae/applications/{application_id} +func ResourceApplication() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceApplicationCreate, + ReadContext: resourceApplicationRead, + DeleteContext: resourceApplicationDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceApplicationImportState, + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Description: "The region where the application is located.", + }, + "environment_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The ID of the environment to which the application belongs.", + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the application.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "The creation time of the application, in RFC3339 format.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "The latest update time of the application, in RFC3339 format.", + }, + }, + } +} + +func buildCreateApplicationBodyParams(d *schema.ResourceData) map[string]interface{} { + return map[string]interface{}{ + "api_version": "v1", + "kind": "Application", + "metadata": map[string]interface{}{ + "name": d.Get("name"), + }, + } +} + +func resourceApplicationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + httpUrl = "v1/{project_id}/cae/applications" + envId = d.Get("environment_id").(string) + ) + client, err := cfg.NewServiceClient("cae", region) + if err != nil { + return diag.Errorf("error creating CAE client: %s", err) + } + + createPath := client.Endpoint + httpUrl + createPath = strings.ReplaceAll(createPath, "{project_id}", client.ProjectID) + + createOpts := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "X-Environment-ID": envId, + "Content-Type": "application/json", + }, + JSONBody: utils.RemoveNil(buildCreateApplicationBodyParams(d)), + } + requestResp, err := client.Request("POST", createPath, &createOpts) + if err != nil { + return diag.Errorf("error creating application: %s", err) + } + + respBody, err := utils.FlattenResponse(requestResp) + if err != nil { + return diag.FromErr(err) + } + + appId := utils.PathSearch("metadata.id", respBody, "").(string) + if appId == "" { + return diag.Errorf("unable to find the application ID from the API response") + } + d.SetId(appId) + + return resourceApplicationRead(ctx, d, meta) +} + +func GetApplicationById(client *golangsdk.ServiceClient, envId, appId string) (interface{}, error) { + httpUrl := "v1/{project_id}/cae/applications/{application_id}" + getPath := client.Endpoint + httpUrl + getPath = strings.ReplaceAll(getPath, "{project_id}", client.ProjectID) + getPath = strings.ReplaceAll(getPath, "{application_id}", appId) + + getOpts := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "X-Environment-ID": envId, + "Content-Type": "application/json", + }, + } + requestResp, err := client.Request("GET", getPath, &getOpts) + if err != nil { + return nil, err + } + + respBody, err := utils.FlattenResponse(requestResp) + if err != nil { + return nil, err + } + return utils.PathSearch("metadata", respBody, nil), nil +} + +func resourceApplicationRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + appId = d.Id() + envId = d.Get("environment_id").(string) + ) + client, err := cfg.NewServiceClient("cae", region) + if err != nil { + return diag.Errorf("error creating CAE client: %s", err) + } + + app, err := GetApplicationById(client, envId, appId) + if err != nil { + // 500 error returned if the application is not exist. + // 400 error returned if the related environment is not exist. + return common.CheckDeletedDiag(d, common.ConvertExpected400ErrInto404Err( + common.ConvertExpected500ErrInto404Err(err, "error_code", appResourceNotFoundCodes...), + "error_code", + envResourceNotFoundCodes...), fmt.Sprintf("error querying application by its ID (%s)", appId)) + } + + mErr := multierror.Append( + d.Set("region", region), + d.Set("name", utils.PathSearch("name", app, nil)), + d.Set("created_at", utils.FormatTimeStampRFC3339(utils.ConvertTimeStrToNanoTimestamp(utils.PathSearch("created_at", + app, "").(string))/1000, false)), + d.Set("updated_at", utils.FormatTimeStampRFC3339(utils.ConvertTimeStrToNanoTimestamp(utils.PathSearch("updated_at", + app, "").(string))/1000, false)), + ) + return diag.FromErr(mErr.ErrorOrNil()) +} + +func resourceApplicationDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + httpUrl = "v1/{project_id}/cae/applications/{application_id}" + envId = d.Get("environment_id").(string) + appId = d.Id() + ) + client, err := cfg.NewServiceClient("cae", region) + if err != nil { + return diag.Errorf("error creating CAE client: %s", err) + } + + deletePath := client.Endpoint + httpUrl + deletePath = strings.ReplaceAll(deletePath, "{project_id}", client.ProjectID) + deletePath = strings.ReplaceAll(deletePath, "{application_id}", appId) + + deleteOpts := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "X-Environment-ID": envId, + "Content-Type": "application/json", + }, + } + _, err = client.Request("DELETE", deletePath, &deleteOpts) + if err != nil { + // 400 error returned if the related environment is not exist. + return common.CheckDeletedDiag(d, common.ConvertExpected400ErrInto404Err(err, "error_code", envResourceNotFoundCodes...), + fmt.Sprintf("error deleting application (%s)", appId)) + } + return nil +} + +func resourceApplicationImportState(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + importedId := d.Id() + parts := strings.Split(importedId, "/") + 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("environment_id", parts[0]) +}