diff --git a/.changelog/44767.txt b/.changelog/44767.txt new file mode 100644 index 000000000000..f5ba486805cf --- /dev/null +++ b/.changelog/44767.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_ssm_parameter_version_labels +``` diff --git a/internal/service/ssm/exports_test.go b/internal/service/ssm/exports_test.go index d4520ed3bfbf..d881aadea18e 100644 --- a/internal/service/ssm/exports_test.go +++ b/internal/service/ssm/exports_test.go @@ -13,6 +13,7 @@ var ( ResourceMaintenanceWindowTarget = resourceMaintenanceWindowTarget ResourceMaintenanceWindowTask = resourceMaintenanceWindowTask ResourceParameter = resourceParameter + ResourceParameterVersionLabels = resourceParameterVersionLabels ResourcePatchBaseline = resourcePatchBaseline ResourcePatchGroup = resourcePatchGroup ResourceResourceDataSync = resourceResourceDataSync @@ -27,8 +28,11 @@ var ( FindMaintenanceWindowTargetByTwoPartKey = findMaintenanceWindowTargetByTwoPartKey FindMaintenanceWindowTaskByTwoPartKey = findMaintenanceWindowTaskByTwoPartKey FindParameterByName = findParameterByName + FindParameterVersionLabels = findParameterVersionLabels FindPatchBaselineByID = findPatchBaselineByID FindPatchGroupByTwoPartKey = findPatchGroupByTwoPartKey FindResourceDataSyncByName = findResourceDataSyncByName FindServiceSettingByID = findServiceSettingByID + + ParameterVersionLabelsParseResourceID = parameterVersionLabelsParseResourceID ) diff --git a/internal/service/ssm/parameter_version_labels.go b/internal/service/ssm/parameter_version_labels.go new file mode 100644 index 000000000000..70a2ec4e5587 --- /dev/null +++ b/internal/service/ssm/parameter_version_labels.go @@ -0,0 +1,267 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ssm + +import ( + "context" + "fmt" + "strings" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKResource("aws_ssm_parameter_version_labels", name="Parameter Version Labels", supportsTags=false) +func resourceParameterVersionLabels() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceParameterVersionLabelsCreate, + ReadWithoutTimeout: resourceParameterVersionLabelsRead, + UpdateWithoutTimeout: resourceParameterVersionLabelsUpdate, + DeleteWithoutTimeout: resourceParameterVersionLabelsDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceParameterVersionLabelsImport, + }, + + Schema: map[string]*schema.Schema{ + names.AttrName: { + Type: schema.TypeString, + Required: true, + }, + names.AttrVersion: { + Type: schema.TypeInt, + Optional: true, + }, + "labels": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validation.AllDiag( + // A label can have a maximum of 100 characters. + validation.ToDiagFunc(validation.StringLenBetween(1, 100)), + // Labels can contain letters (case sensitive), numbers, periods (.), hyphens (-), or underscores (_). + // Labels can't begin with a number. + validation.ToDiagFunc(validation.StringMatch(regexache.MustCompile(`^[a-zA-Z_][a-zA-Z0-9._-]*$`), "must begin with a letter or underscore and contain only letters, numbers, periods (.), hyphens (-), or underscores (_)")), + // Labels can't begin with " aws " or " ssm " (not case sensitive). + func(v any, p cty.Path) diag.Diagnostics { + if str, ok := v.(string); ok { + if strings.HasPrefix(str, "aws") || strings.HasPrefix(str, "ssm") { + return diag.Diagnostics{diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid label", + Detail: "Labels cannot start with 'aws' or 'ssm'", + AttributePath: p, + }} + } + } + return nil + }, + ), + }, + }, + }, + } +} + +func resourceParameterVersionLabelsCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).SSMClient(ctx) + + version := d.Get(names.AttrVersion).(int) + name := d.Get(names.AttrName).(string) + labels := d.Get("labels").([]any) + // we do not have parameter version, getting the latest + if version == 0 { + input := &ssm.GetParameterInput{ + Name: aws.String(name), + } + output, err := conn.GetParameter(ctx, input) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading SSM Parameter (%s) latest version: %s", name, err) + } + if output.Parameter != nil { + version = int(output.Parameter.Version) + } else { + return sdkdiag.AppendErrorf(diags, "reading SSM Parameter (%s) latest version: parameter not found", name) + } + } + input := &ssm.LabelParameterVersionInput{ + Name: aws.String(name), + Labels: tfslices.ApplyToAll(labels, func(l any) string { return l.(string) }), + } + output, err := conn.LabelParameterVersion(ctx, input) + if err != nil { + return sdkdiag.AppendErrorf(diags, "labeling SSM Parameter (%s) version (%d) labels (%v): %s", name, version, labels, err) + } + version = int(output.ParameterVersion) + d.SetId(fmt.Sprintf("%s:%d", name, version)) + return diags +} + +func resourceParameterVersionLabelsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).SSMClient(ctx) + + version := d.Get(names.AttrVersion).(int) + name := d.Get(names.AttrName).(string) + labels, err := findParameterVersionLabels(ctx, conn, name, version) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + d.Set("labels", labels) + d.SetId(fmt.Sprintf("%s:%d", name, version)) + return diags +} + +func resourceParameterVersionLabelsUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).SSMClient(ctx) + + version := d.Get(names.AttrVersion).(int) + name := d.Get(names.AttrName).(string) + labels := d.Get("labels").([]any) + // we do not have parameter version, getting the latest + if version == 0 { + input := &ssm.GetParameterInput{ + Name: aws.String(name), + } + output, err := conn.GetParameter(ctx, input) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading SSM Parameter (%s) latest version: %s", name, err) + } + if output.Parameter != nil { + version = int(output.Parameter.Version) + } else { + return sdkdiag.AppendErrorf(diags, "reading SSM Parameter (%s) latest version: parameter not found", name) + } + } + input := &ssm.LabelParameterVersionInput{ + Name: aws.String(name), + Labels: tfslices.ApplyToAll(labels, func(l any) string { return l.(string) }), + } + output, err := conn.LabelParameterVersion(ctx, input) + if err != nil { + return sdkdiag.AppendErrorf(diags, "labeling SSM Parameter (%s) version (%d) labels (%v): %s", name, version, labels, err) + } + version = int(output.ParameterVersion) + d.SetId(fmt.Sprintf("%s:%d", name, version)) + return diags +} + +func resourceParameterVersionLabelsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).SSMClient(ctx) + + version := d.Get(names.AttrVersion).(int) + name := d.Get(names.AttrName).(string) + labels := d.Get("labels").([]any) + // we do not have parameter version, getting the latest + if version == 0 { + input := &ssm.GetParameterInput{ + Name: aws.String(name), + } + output, err := conn.GetParameter(ctx, input) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading SSM Parameter (%s) latest version: %s", name, err) + } + if output.Parameter != nil { + version = int(output.Parameter.Version) + } else { + return sdkdiag.AppendErrorf(diags, "reading SSM Parameter (%s) latest version: parameter not found", name) + } + } + input := &ssm.UnlabelParameterVersionInput{ + Name: aws.String(name), + ParameterVersion: aws.Int64(int64(version)), + Labels: tfslices.ApplyToAll(labels, func(l any) string { return l.(string) }), + } + _, err := conn.UnlabelParameterVersion(ctx, input) + if err != nil { + return sdkdiag.AppendErrorf(diags, "labeling SSM Parameter (%s) version (%d) labels (%v): %s", name, version, labels, err) + } + return diags +} + +func findParameterVersionLabels(ctx context.Context, conn *ssm.Client, name string, version int) ([]string, error) { + // we do not have parameter version, getting the latest + if version == 0 { + input := &ssm.GetParameterInput{ + Name: aws.String(name), + } + output, err := conn.GetParameter(ctx, input) + if err != nil { + return nil, fmt.Errorf("reading SSM Parameter (%s) latest version: %w", name, err) + } + if output.Parameter != nil { + version = int(output.Parameter.Version) + } else { + return nil, fmt.Errorf("reading SSM Parameter (%s) latest version: parameter not found", name) + } + } + input := &ssm.GetParameterHistoryInput{ + Name: aws.String(name), + MaxResults: aws.Int32(10), + } + pages := ssm.NewGetParameterHistoryPaginator(conn, input) + found := false + var labels []string + for pages.HasMorePages() && !found { + output, err := pages.NextPage(ctx) + if err != nil { + return nil, fmt.Errorf("reading SSM Parameter (%s) labels: %w", name, err) + } + for _, param := range output.Parameters { + if param.Version != int64(version) { + continue + } + labels = append(labels, param.Labels...) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("reading SSM Parameter (%s) labels: version %d not found", name, version) + } + return labels, nil +} + +func parameterVersionLabelsParseResourceID(id string) (string, int, error) { + parts := strings.SplitN(id, ":", 2) + if len(parts) != 2 { + return "", 0, fmt.Errorf("unexpected format of ID (%s), expected name:version", id) + } + version := 0 + _, err := fmt.Sscanf(parts[1], "%d", &version) + if err != nil { + return "", 0, fmt.Errorf("unexpected format of version in ID (%s), expected integer: %w", id, err) + } + return parts[0], version, nil +} + +func resourceParameterVersionLabelsImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + name, version, err := parameterVersionLabelsParseResourceID(d.Id()) + if err != nil { + return []*schema.ResourceData{}, err + } + + conn := meta.(*conns.AWSClient).SSMClient(ctx) + labels, err := findParameterVersionLabels(ctx, conn, name, version) + if err != nil { + return []*schema.ResourceData{}, err + } + d.Set(names.AttrName, name) + d.Set(names.AttrVersion, version) + d.Set("labels", labels) + return []*schema.ResourceData{d}, nil +} diff --git a/internal/service/ssm/parameter_version_labels_test.go b/internal/service/ssm/parameter_version_labels_test.go new file mode 100644 index 000000000000..fa424561f5cd --- /dev/null +++ b/internal/service/ssm/parameter_version_labels_test.go @@ -0,0 +1,159 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ssm_test + +import ( + "context" + "fmt" + "testing" + + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfssm "github.com/hashicorp/terraform-provider-aws/internal/service/ssm" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// Acceptance test access AWS and cost money to run. +func TestAccSSMParameterVersionLabels_basic(t *testing.T) { + ctx := acctest.Context(t) + // TIP: This is a long-running test guard for tests that run longer than + // 300s (5 min) generally. + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var parameterversionlabels []string + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ssm_parameter_version_labels.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckParameterVersionLabelsDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccParameterVersionLabelsConfig_basic(rName, "1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckParameterVersionLabelsExists(ctx, resourceName, ¶meterversionlabels), + resource.TestCheckResourceAttr(resourceName, "labels.#", "2"), + resource.TestCheckResourceAttr(resourceName, "labels.0", "label1"), + resource.TestCheckResourceAttr(resourceName, "labels.1", "label2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSSMParameterVersionLabels_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var parameterversionlabels []string + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ssm_parameter_version_labels.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckParameterVersionLabelsDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccParameterVersionLabelsConfig_basic(rName, "2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckParameterVersionLabelsExists(ctx, resourceName, ¶meterversionlabels), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfssm.ResourceParameterVersionLabels(), resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func testAccCheckParameterVersionLabelsDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).SSMClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ssm_parameter_version_labels" { + continue + } + + name, version, err := tfssm.ParameterVersionLabelsParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + _, err = tfssm.FindParameterVersionLabels(ctx, conn, name, version) + if retry.NotFound(err) { + return nil + } + if err != nil { + return err + } + + return fmt.Errorf("Parameter Version Labels %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckParameterVersionLabelsExists(ctx context.Context, name string, parameterversionlabels *[]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).SSMClient(ctx) + + name, version, err := tfssm.ParameterVersionLabelsParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + resp, err := tfssm.FindParameterVersionLabels(ctx, conn, name, version) + + if err != nil { + return err + } + + *parameterversionlabels = append(*parameterversionlabels, resp...) + + return nil + } +} + +func testAccParameterVersionLabelsConfig_basic(rName, version string) string { + return fmt.Sprintf(` +resource "aws_ssm_parameter" "test" { + name = %[1]q + type = "String" + value = "test value" +} + +resource "aws_ssm_parameter_version_labels" "test" { + name = aws_ssm_parameter.test.name + version = %[2]q == "" ? 0 : tonumber(%[2]q) + labels = ["label1", "label2"] +} +`, rName, version) +} diff --git a/internal/service/ssm/service_package_gen.go b/internal/service/ssm/service_package_gen.go index 4c89804593fb..dee6dd3673ac 100644 --- a/internal/service/ssm/service_package_gen.go +++ b/internal/service/ssm/service_package_gen.go @@ -183,6 +183,12 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*inttypes.ServicePa CustomImport: true, }, }, + { + Factory: resourceParameterVersionLabels, + TypeName: "aws_ssm_parameter_version_labels", + Name: "Parameter Version Labels", + Region: unique.Make(inttypes.ResourceRegionDefault()), + }, { Factory: resourcePatchBaseline, TypeName: "aws_ssm_patch_baseline", diff --git a/website/docs/r/ssm_ssm_parameter_version_labels.html.markdown b/website/docs/r/ssm_ssm_parameter_version_labels.html.markdown new file mode 100644 index 000000000000..1b9fafa9f39b --- /dev/null +++ b/website/docs/r/ssm_ssm_parameter_version_labels.html.markdown @@ -0,0 +1,76 @@ +--- +subcategory: "SSM (Systems Manager)" +layout: "aws" +page_title: "AWS: aws_ssm_parameter_version_labels" +description: |- + Manages an AWS SSM (Systems Manager) Parameter Version Labels. +--- + + + +# Resource: aws_ssm_parameter_version_labels + +Manages an AWS SSM (Systems Manager) Parameter Version Labels. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_ssm_parameter_version_labels" "example" { + name = "parameter" + labels = ["label1", "label2"] +} +``` + +## Argument Reference + +The following arguments are required: + +- `name` - (Required) SSM Parameter name. +- `labels` - (Required) a list of labels to add + +The following arguments are optional: + +- `version` - (Optional) SSM Parameter version. If omitted, latest parameter version is used + +## Attribute Reference + +This resource exports no additional attributes. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import SSM (Systems Manager) Parameter Version Labels using the `name` or `name:version`. For example: + +```terraform +import { + to = aws_ssm_parameter_version_labels.example + id = "parameter" +} +``` + +```terraform +import { + to = aws_ssm_parameter_version_labels.example + id = "parameter:1" +} +``` + +Using `terraform import`, import SSM (Systems Manager) Parameter Version Labels using the `name` or `name:version`. For example: + +```console +% terraform import aws_ssm_parameter_version_labels.example parameter +``` + +```console +% terraform import aws_ssm_parameter_version_labels.example parameter:1 +```