From 64c107d3f8c89d821a077cb9ae0d90a30dd211c4 Mon Sep 17 00:00:00 2001 From: wuzhuanhong Date: Wed, 15 Jan 2025 17:09:04 +0800 Subject: [PATCH] feat(cae): add new resource to manage certificate --- docs/resources/cae_certificate.md | 66 ++++ huaweicloud/provider.go | 1 + ...source_huaweicloud_cae_certificate_test.go | 107 +++++++ .../resource_huaweicloud_cae_certificate.go | 291 ++++++++++++++++++ 4 files changed, 465 insertions(+) create mode 100644 docs/resources/cae_certificate.md create mode 100644 huaweicloud/services/acceptance/cae/resource_huaweicloud_cae_certificate_test.go create mode 100644 huaweicloud/services/cae/resource_huaweicloud_cae_certificate.go diff --git a/docs/resources/cae_certificate.md b/docs/resources/cae_certificate.md new file mode 100644 index 0000000000..93fa5d4efc --- /dev/null +++ b/docs/resources/cae_certificate.md @@ -0,0 +1,66 @@ +--- +subcategory: "Cloud Application Engine (CAE)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_cae_certificate" +description: |- + Manages a certificate resource within HuaweiCloud. +--- + +# huaweicloud_cae_certificate + +Manages a certificate resource within HuaweiCloud. + +## Example Usage + +```hcl +variable "environment_id" {} +variable "certificate_name" {} +variable "certificate_content" {} +variable "certificate_private_key" {} + +resource "huaweicloud_cae_certificate" "test" { + environment_id = var.environment_id + name = var.certificate_name + crt = var.certificate_content + key = var.certificate_private_key +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region in which to create the resource. + 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 CAE environment. + Changing this creates a new resource. + +* `name` - (Required, String, ForceNew) Specifies the name of the certificate. + Changing this creates a new resource. + The maximum length of the name is `64` characters, only lowercase letters, digits, hyphens (-) and dots (.) are + allowed. + The name must start and end with a lowercase letter or a digit. + +* `crt` - (Required, String) Specifies the content of the certificate. + Base64 format corresponding to PEM encoding. + +* `key` - (Required, String) Specifies the private key of the certificate. + Base64 format corresponding to PEM encoding. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID. + +* `created_at` - The creation time of the certificate, in RFC3339 format. + +## Import + +The certificate resource can be imported using `environment_id` and `name`, separated by a slash (/), e.g. + +```bash +$ terraform import huaweicloud_cae_certificate.test / +``` diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index a5b5c395de..72aca74e49 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1433,6 +1433,7 @@ func Provider() *schema.Provider { "huaweicloud_bcs_instance": bcs.ResourceInstance(), "huaweicloud_cae_application": cae.ResourceApplication(), + "huaweicloud_cae_certificate": cae.ResourceCertificate(), "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_certificate_test.go b/huaweicloud/services/acceptance/cae/resource_huaweicloud_cae_certificate_test.go new file mode 100644 index 0000000000..04a7939769 --- /dev/null +++ b/huaweicloud/services/acceptance/cae/resource_huaweicloud_cae_certificate_test.go @@ -0,0 +1,107 @@ +package cae + +import ( + "fmt" + "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 getCertificateFunc(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) + } + + environmentId := state.Primary.Attributes["environment_id"] + return cae.GetCertificateById(client, environmentId, state.Primary.ID) +} + +func TestAccCertificate_basic(t *testing.T) { + var ( + obj interface{} + + name = acceptance.RandomAccResourceNameWithDash() + + rName = "huaweicloud_cae_certificate.test" + rc = acceptance.InitResourceCheck( + rName, + &obj, + getCertificateFunc, + ) + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + acceptance.TestAccPreCheckCaeEnvironment(t) + acceptance.TestAccPreCheckCertificateWithoutRootCA(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccCertificate_basic(name, acceptance.HW_CERTIFICATE_CONTENT, acceptance.HW_CERTIFICATE_PRIVATE_KEY), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "environment_id", acceptance.HW_CAE_ENVIRONMENT_ID), + resource.TestCheckResourceAttr(rName, "name", name), + resource.TestCheckResourceAttr(rName, "crt", acceptance.HW_CERTIFICATE_CONTENT), + resource.TestCheckResourceAttr(rName, "key", acceptance.HW_CERTIFICATE_PRIVATE_KEY), + ), + }, + { + Config: testAccCertificate_basic(name, acceptance.HW_NEW_CERTIFICATE_CONTENT, acceptance.HW_NEW_CERTIFICATE_PRIVATE_KEY), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "crt", acceptance.HW_NEW_CERTIFICATE_CONTENT), + resource.TestCheckResourceAttr(rName, "key", acceptance.HW_NEW_CERTIFICATE_PRIVATE_KEY), + ), + }, + { + ResourceName: rName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccCertificateImportStateFunc(rName), + }, + }, + }) +} + +func testAccCertificate_basic(name, content, privateKey string) string { + return fmt.Sprintf(` + +resource "huaweicloud_cae_certificate" "test" { + environment_id = "%[1]s" + name = "%[2]s" + # Base64 format corresponding to PEM encoding + crt = "%[3]s" + key = "%[4]s" +} + `, acceptance.HW_CAE_ENVIRONMENT_ID, name, content, privateKey) +} + +func testAccCertificateImportStateFunc(name string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[name] + if !ok { + return "", fmt.Errorf("resource (%s) not found: %s", name, rs) + } + + var ( + environmentId = rs.Primary.Attributes["environment_id"] + certificateName = rs.Primary.Attributes["name"] + ) + if environmentId == "" || certificateName == "" { + return "", fmt.Errorf("some import IDs are missing, want '/', but got '%s/%s'", + environmentId, certificateName) + } + + return fmt.Sprintf("%s/%s", environmentId, certificateName), nil + } +} diff --git a/huaweicloud/services/cae/resource_huaweicloud_cae_certificate.go b/huaweicloud/services/cae/resource_huaweicloud_cae_certificate.go new file mode 100644 index 0000000000..8c5f92df21 --- /dev/null +++ b/huaweicloud/services/cae/resource_huaweicloud_cae_certificate.go @@ -0,0 +1,291 @@ +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 certificateResourceNotFoundCodes = []string{ + "CAE.01500404", // The environment not found. + "CAE.01500005", // The resource not found. +} + +// @API CAE POST /v1/{project_id}/cae/certificates +// @API CAE GET /v1/{project_id}/cae/certificates +// @API CAE PUT /v1/{project_id}/cae/certificates/{certificate_id} +// @API CAE DELETE /v1/{project_id}/cae/certificates/{certificate_id} +func ResourceCertificate() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceCertificateCreate, + ReadContext: resourceCertificateRead, + UpdateContext: resourceCertificateUpdate, + DeleteContext: resourceCertificateDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceCertificateImportState, + }, + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "environment_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The ID of the CAE environment.`, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The name of the certificate.`, + }, + "crt": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: `The content of the certificate.`, + }, + "key": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: `The private key of the certificate.`, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: `The creation time of the certificate, in RFC3339 format.`, + }, + }, + } +} + +func buildCertificateBodyParams(d *schema.ResourceData, isCreated bool) map[string]interface{} { + params := map[string]interface{}{ + "api_version": "v1", + "kind": "Certificate", + "spec": map[string]interface{}{ + "crt": d.Get("crt"), + "key": d.Get("key"), + }, + } + + if isCreated { + params["metadata"] = map[string]interface{}{ + "name": d.Get("name"), + } + } + return params +} + +func resourceCertificateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + httpUrl = "v1/{project_id}/cae/certificates" + environmentId = 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": environmentId, + }, + JSONBody: buildCertificateBodyParams(d, true), + } + createResp, err := client.Request("POST", createPath, &createOpts) + if err != nil { + return diag.Errorf("error creating certificate for environment (%s): %s", environmentId, err) + } + + createRespBody, err := utils.FlattenResponse(createResp) + if err != nil { + return diag.FromErr(err) + } + + certificateId := utils.PathSearch("items|[0].metadata.id", createRespBody, "").(string) + if certificateId == "" { + return diag.Errorf("unable to find the certificate ID from the API response") + } + + d.SetId(certificateId) + return resourceCertificateRead(ctx, d, meta) +} + +func GetCertificateById(client *golangsdk.ServiceClient, environmentId, certificateId string) (interface{}, error) { + certificateInfos, err := getCertificates(client, environmentId) + if err != nil { + return nil, common.ConvertExpected400ErrInto404Err(err, "error_code", envResourceNotFoundCodes...) + } + + certificateInfo := utils.PathSearch(fmt.Sprintf("items[?metadata.id=='%s']|[0]", certificateId), certificateInfos, nil) + if certificateInfo == nil { + return nil, golangsdk.ErrDefault404{} + } + + return certificateInfo, nil +} + +func getCertificates(client *golangsdk.ServiceClient, environmentId string) (interface{}, error) { + httpUrl := "v1/{project_id}/cae/certificates" + listPath := client.Endpoint + httpUrl + listPath = strings.ReplaceAll(listPath, "{project_id}", client.ProjectID) + listOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "X-Environment-Id": environmentId, + }, + } + resp, err := client.Request("GET", listPath, &listOpt) + if err != nil { + return nil, err + } + + return utils.FlattenResponse(resp) +} + +func resourceCertificateRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + ) + + client, err := cfg.NewServiceClient("cae", region) + if err != nil { + return diag.Errorf("error creating CAE client: %s", err) + } + + certificateInfo, err := GetCertificateById(client, d.Get("environment_id").(string), d.Id()) + if err != nil { + return common.CheckDeletedDiag(d, err, "error retrieving certificate") + } + + mErr := multierror.Append( + d.Set("region", region), + d.Set("name", utils.PathSearch("metadata.name", certificateInfo, nil)), + d.Set("crt", utils.PathSearch("spec.crt", certificateInfo, nil)), + d.Set("key", utils.PathSearch("spec.key", certificateInfo, nil)), + d.Set("created_at", utils.FormatTimeStampRFC3339(utils.ConvertTimeStrToNanoTimestamp(utils.PathSearch("spec.created_at", + certificateInfo, "").(string))/1000, false)), + ) + return diag.FromErr(mErr.ErrorOrNil()) +} + +func resourceCertificateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + client, err := cfg.NewServiceClient("cae", cfg.GetRegion(d)) + if err != nil { + return diag.Errorf("error creating CAE client: %s", err) + } + + if d.HasChanges("crt", "key") { + httpUrl := "v1/{project_id}/cae/certificates/{certificate_id}" + updatePath := client.Endpoint + httpUrl + updatePath = strings.ReplaceAll(updatePath, "{project_id}", client.ProjectID) + updatePath = strings.ReplaceAll(updatePath, "{certificate_id}", d.Id()) + updateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "X-Environment-Id": d.Get("environment_id").(string), + }, + JSONBody: buildCertificateBodyParams(d, false), + OkCodes: []int{204}, + } + + _, err = client.Request("PUT", updatePath, &updateOpt) + if err != nil { + return diag.Errorf("error updating certificate (%s): %s", d.Get("name").(string), err) + } + } + + return resourceCertificateRead(ctx, d, meta) +} + +func resourceCertificateDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + httpUrl = "v1/{project_id}/cae/certificates/{certificate_id}" + ) + client, err := cfg.NewServiceClient("cae", cfg.GetRegion(d)) + 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, "{certificate_id}", d.Id()) + deleteOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "X-Environment-Id": d.Get("environment_id").(string), + }, + } + + _, err = client.Request("DELETE", deletePath, &deleteOpt) + if err != nil { + return common.CheckDeletedDiag(d, common.ConvertExpected400ErrInto404Err(err, "error_code", certificateResourceNotFoundCodes...), + fmt.Sprintf("error deleting certificate (%s)", d.Get("name").(string))) + } + + return nil +} + +func resourceCertificateImportState(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + var ( + cfg = meta.(*config.Config) + 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) + } + + mErr := multierror.Append(nil, + d.Set("environment_id", parts[0]), + ) + if mErr.ErrorOrNil() != nil { + return nil, mErr + } + + client, err := cfg.NewServiceClient("cae", cfg.GetRegion(d)) + if err != nil { + return nil, fmt.Errorf("error creating CAE client: %s", err) + } + + certificates, err := getCertificates(client, parts[0]) + if err != nil { + return nil, fmt.Errorf("error retrieving the list of the certificates: %s", err) + } + + certificateId := utils.PathSearch(fmt.Sprintf("items[?metadata.name=='%s']|[0].metadata.id", parts[1]), certificates, "").(string) + if certificateId == "" { + return nil, fmt.Errorf("unable to find the ID of the certificate (%s) from API response : %s", parts[1], err) + } + + d.SetId(certificateId) + return []*schema.ResourceData{d}, nil +}