diff --git a/docs/resources/csms_secret_version_state.md b/docs/resources/csms_secret_version_state.md new file mode 100644 index 0000000000..d4fa06dc3a --- /dev/null +++ b/docs/resources/csms_secret_version_state.md @@ -0,0 +1,64 @@ +--- +subcategory: "Data Encryption Workshop (DEW)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_csms_secret_version_state" +description: | + Manages a CSMS secret version state resource within HuaweiCloud. +--- + +# huaweicloud_csms_secret_version_state + +Manages a CSMS secret version state resource within HuaweiCloud. + +-> A secret supports a maximum of `12` secret version states, each secret version state can identify only one + secret version. +
If you add a secret version state in use to a new secret version, the secret version state will be + automatically removed from the old secret version. +
**SYSCURRENT** and **SYSPREVIOUS** are built-in states, not support deletion. + +## Example Usage + +```hcl +variable "secret_name" {} +variable "name" {} +variable "version_id" {} + +resource "huaweicloud_csms_secret_version_state" "test" { + secret_name = var.secret_name + name = var.name + version_id = var.version_id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region in which to create the CSMS secret version state. + If omitted, the provider-level region will be used. Changing this parameter will create a new resource. + +* `secret_name` - (Required, String, ForceNew) Specifies the name of the secret to which the secret version state + belongs. Changing this parameter will create a new resource. + +* `name` - (Required, String, ForceNew) Specifies the name of the secret version state. + Changing this parameter will create a new secret version. + Only letters, digits, underscores(_) and hyphens(-) are allowed. + The valid length is limited from `1` to `64` characters. + +* `version_id` - (Required, String) Specifies the ID of the secret version. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID, same as `name`. + +* `updated_at` - The last update time of the secret version state, in RFC3339 format. + +## Import + +The secret version state can be imported using the related `secret_name` and their `id`, separated by a slash (/), e.g. + +```bash +terraform import huaweicloud_csms_secret_version_state.test / +``` diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index 1b3b8c6f99..3bfbc36eb5 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1288,8 +1288,9 @@ func Provider() *schema.Provider { "huaweicloud_cse_microservice_engine": cse.ResourceMicroserviceEngine(), "huaweicloud_cse_microservice_instance": cse.ResourceMicroserviceInstance(), - "huaweicloud_csms_event": dew.ResourceCsmsEvent(), - "huaweicloud_csms_secret": dew.ResourceSecret(), + "huaweicloud_csms_event": dew.ResourceCsmsEvent(), + "huaweicloud_csms_secret": dew.ResourceSecret(), + "huaweicloud_csms_secret_version_state": dew.ResourceSecretVersionState(), "huaweicloud_css_cluster": css.ResourceCssCluster(), "huaweicloud_css_cluster_restart": css.ResourceCssClusterRestart(), diff --git a/huaweicloud/services/acceptance/dew/resource_huaweicloud_csms_secret_version_state_test.go b/huaweicloud/services/acceptance/dew/resource_huaweicloud_csms_secret_version_state_test.go new file mode 100644 index 0000000000..d665b0fd34 --- /dev/null +++ b/huaweicloud/services/acceptance/dew/resource_huaweicloud_csms_secret_version_state_test.go @@ -0,0 +1,143 @@ +package dew + +import ( + "fmt" + "regexp" + "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/utils" +) + +func getSecretVersionStateResourceFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) { + var ( + region = acceptance.HW_REGION_NAME + getVersionStatehttpUrl = "v1/{project_id}/secrets/{secret_name}/stages/{stage_name}" + product = "kms" + ) + client, err := cfg.NewServiceClient(product, region) + if err != nil { + return nil, fmt.Errorf("error creating KMS client: %s", err) + } + + getVersionStatePath := client.Endpoint + getVersionStatehttpUrl + getVersionStatePath = strings.ReplaceAll(getVersionStatePath, "{project_id}", client.ProjectID) + getVersionStatePath = strings.ReplaceAll(getVersionStatePath, "{secret_name}", state.Primary.Attributes["secret_name"]) + getVersionStatePath = strings.ReplaceAll(getVersionStatePath, "{stage_name}", state.Primary.ID) + getVersionStateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + + getResp, err := client.Request("GET", getVersionStatePath, &getVersionStateOpt) + if err != nil { + return nil, fmt.Errorf("error retrieving secret version state: %s", err) + } + return utils.FlattenResponse(getResp) +} + +func TestAccSecretVersionState_basic(t *testing.T) { + var obj interface{} + + name := acceptance.RandomAccResourceName() + rName := "huaweicloud_csms_secret_version_state.test" + + rc := acceptance.InitResourceCheck( + rName, + &obj, + getSecretVersionStateResourceFunc, + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccSecretVersionState_basic(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(rName, "secret_name", "huaweicloud_csms_secret.test", "name"), + resource.TestCheckResourceAttr(rName, "name", name), + resource.TestCheckResourceAttrPair(rName, "version_id", "huaweicloud_csms_secret.test", "latest_version"), + resource.TestMatchResourceAttr(rName, "updated_at", + regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)), + ), + }, + { + Config: testAccSecretVersionState_update(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "version_id", "v2"), + resource.TestCheckResourceAttr(rName, "name", name), + resource.TestMatchResourceAttr(rName, "updated_at", + regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)), + ), + }, + { + ResourceName: rName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccSecretVersionStateImportStateFunc(rName), + }, + }, + }) +} + +func testAccSecretVersionStateImportStateFunc(name string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + var secretName, stateId string + rs, ok := s.RootModule().Resources[name] + if !ok { + return "", fmt.Errorf("the resource (%s) not found", name) + } + + secretName = rs.Primary.Attributes["secret_name"] + stateId = rs.Primary.ID + if secretName == "" || stateId == "" { + return "", fmt.Errorf("invalid format specified for import ID, want '/', but got '%s/%s'", + secretName, stateId) + } + return fmt.Sprintf("%s/%s", secretName, stateId), nil + } +} + +func testAccSecretVersionState_basic(name string) string { + return fmt.Sprintf(` +resource "huaweicloud_csms_secret" "test" { + name = "%[1]s" + secret_text = "secret version" + description = "acc test" +} + +resource "huaweicloud_csms_secret_version_state" "test" { + secret_name = huaweicloud_csms_secret.test.name + name = "%[1]s" + version_id = huaweicloud_csms_secret.test.latest_version +} +`, name) +} + +func testAccSecretVersionState_update(name string) string { + return fmt.Sprintf(` +resource "huaweicloud_csms_secret" "test" { + name = "%[1]s" + secret_text = "version state" + description = "acc test" +} + +resource "huaweicloud_csms_secret_version_state" "test" { + secret_name = huaweicloud_csms_secret.test.name + name = "%[1]s" + version_id = "v2" +} +`, name) +} diff --git a/huaweicloud/services/dew/resource_huaweicloud_csms_secret_version_state.go b/huaweicloud/services/dew/resource_huaweicloud_csms_secret_version_state.go new file mode 100644 index 0000000000..777e51505d --- /dev/null +++ b/huaweicloud/services/dew/resource_huaweicloud_csms_secret_version_state.go @@ -0,0 +1,241 @@ +package dew + +import ( + "context" + "fmt" + "net/http" + "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" +) + +// @API DEW PUT /v1/{project_id}/secrets/{secret_name}/stages/{stage_name} +// @API DEW GET /v1/{project_id}/secrets/{secret_name}/stages/{stage_name} +// @API DEW DELETE /v1/{project_id}/secrets/{secret_name}/stages/{stage_name} +func ResourceSecretVersionState() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSecretVersionStateCreate, + ReadContext: resourceSecretVersionStateRead, + UpdateContext: resourceSecretVersionStateUpdate, + DeleteContext: resourceSecretVersionStateDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceSecretVersionStateImportState, + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "secret_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "version_id": { + Type: schema.TypeString, + Required: true, + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceSecretVersionStateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + createVersionStateHttpUrl = "v1/{project_id}/secrets/{secret_name}/stages/{stage_name}" + product = "kms" + ) + client, err := cfg.NewServiceClient(product, region) + if err != nil { + return diag.Errorf("error creating KMS client: %s", err) + } + + createVersionStatePath := client.Endpoint + createVersionStateHttpUrl + createVersionStatePath = strings.ReplaceAll(createVersionStatePath, "{project_id}", client.ProjectID) + createVersionStatePath = strings.ReplaceAll(createVersionStatePath, "{secret_name}", d.Get("secret_name").(string)) + createVersionStatePath = strings.ReplaceAll(createVersionStatePath, "{stage_name}", d.Get("name").(string)) + + createVersionStateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + JSONBody: buildSecretVersionStateBodyParams(d), + } + + creatVersionStateResp, err := client.Request("PUT", createVersionStatePath, &createVersionStateOpt) + if err != nil { + return diag.Errorf("error creating secret version state: %s", err) + } + + createVersionStateRespBody, err := utils.FlattenResponse(creatVersionStateResp) + if err != nil { + return diag.FromErr(err) + } + + stateName := utils.PathSearch("stage.name", createVersionStateRespBody, "").(string) + if stateName == "" { + return diag.Errorf("error creating secret version state: the name is not found in API response") + } + d.SetId(stateName) + + return resourceSecretVersionStateRead(ctx, d, meta) +} + +func buildSecretVersionStateBodyParams(d *schema.ResourceData) map[string]interface{} { + bodyParams := map[string]interface{}{ + "version_id": d.Get("version_id"), + } + return bodyParams +} + +func getVersionStateInfo(client *golangsdk.ServiceClient, d *schema.ResourceData) (*http.Response, error) { + getVersionStatehttpUrl := "v1/{project_id}/secrets/{secret_name}/stages/{stage_name}" + getVersionStatePath := client.Endpoint + getVersionStatehttpUrl + getVersionStatePath = strings.ReplaceAll(getVersionStatePath, "{project_id}", client.ProjectID) + getVersionStatePath = strings.ReplaceAll(getVersionStatePath, "{secret_name}", d.Get("secret_name").(string)) + getVersionStatePath = strings.ReplaceAll(getVersionStatePath, "{stage_name}", d.Id()) + getVersionStateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + + return client.Request("GET", getVersionStatePath, &getVersionStateOpt) +} + +func resourceSecretVersionStateRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + mErr *multierror.Error + product = "kms" + ) + client, err := cfg.NewServiceClient(product, region) + if err != nil { + return diag.Errorf("error creating KMS client: %s", err) + } + + getVersionStateResp, err := getVersionStateInfo(client, d) + if err != nil { + return common.CheckDeletedDiag(d, err, "error retrieving secret version state") + } + + getVersionStateRespBody, err := utils.FlattenResponse(getVersionStateResp) + if err != nil { + return diag.FromErr(err) + } + + mErr = multierror.Append( + mErr, + d.Set("name", utils.PathSearch("stage.name", getVersionStateRespBody, nil)), + d.Set("secret_name", utils.PathSearch("stage.secret_name", getVersionStateRespBody, nil)), + d.Set("version_id", utils.PathSearch("stage.version_id", getVersionStateRespBody, nil)), + d.Set("updated_at", utils.FormatTimeStampRFC3339( + int64(utils.PathSearch("stage.update_time", getVersionStateRespBody, float64(0)).(float64))/1000, false)), + ) + + return diag.FromErr(mErr.ErrorOrNil()) +} + +func resourceSecretVersionStateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + updateVersionStateHttpUrl = "v1/{project_id}/secrets/{secret_name}/stages/{stage_name}" + product = "kms" + ) + client, err := cfg.NewServiceClient(product, region) + if err != nil { + return diag.Errorf("error creating KMS client: %s", err) + } + + updateVersionStatePath := client.Endpoint + updateVersionStateHttpUrl + updateVersionStatePath = strings.ReplaceAll(updateVersionStatePath, "{project_id}", client.ProjectID) + updateVersionStatePath = strings.ReplaceAll(updateVersionStatePath, "{secret_name}", d.Get("secret_name").(string)) + updateVersionStatePath = strings.ReplaceAll(updateVersionStatePath, "{stage_name}", d.Id()) + updateVersionStateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + JSONBody: buildSecretVersionStateBodyParams(d), + } + + _, err = client.Request("PUT", updateVersionStatePath, &updateVersionStateOpt) + if err != nil { + return diag.Errorf("error updating secret version state: %s", err) + } + + return resourceSecretVersionStateRead(ctx, d, meta) +} + +func resourceSecretVersionStateDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + deleteVersionStateHttpUrl = "v1/{project_id}/secrets/{secret_name}/stages/{stage_name}" + product = "kms" + ) + client, err := cfg.NewServiceClient(product, region) + if err != nil { + return diag.Errorf("error creating KMS client: %s", err) + } + + deleteVersionStatePath := client.Endpoint + deleteVersionStateHttpUrl + deleteVersionStatePath = strings.ReplaceAll(deleteVersionStatePath, "{project_id}", client.ProjectID) + deleteVersionStatePath = strings.ReplaceAll(deleteVersionStatePath, "{secret_name}", d.Get("secret_name").(string)) + deleteVersionStatePath = strings.ReplaceAll(deleteVersionStatePath, "{stage_name}", d.Id()) + deleteVersionStateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + OkCodes: []int{ + 204, + }, + } + + // Before deleting, call the query details API, if query no result , then process `CheckDeleted` logic. + _, err = getVersionStateInfo(client, d) + if err != nil { + return common.CheckDeletedDiag(d, err, "error retrieving secret version state") + } + + _, err = client.Request("DELETE", deleteVersionStatePath, &deleteVersionStateOpt) + if err != nil { + return diag.Errorf("error deleting secret version state: %s", err) + } + + // Successful deletion API call does not guarantee that the resource is successfully deleted. + // Call the details API to confirm that the resource has been successfully deleted. + _, err = getVersionStateInfo(client, d) + if err == nil { + return diag.Errorf("error deleting secret version state: the version state still exists") + } + + return nil +} + +func resourceSecretVersionStateImportState(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid format for import ID, want '/', but got '%s'", d.Id()) + } + + d.SetId(parts[1]) + mErr := multierror.Append(nil, + d.Set("secret_name", parts[0]), + ) + return []*schema.ResourceData{d}, mErr.ErrorOrNil() +}