diff --git a/.changelog/45326.txt b/.changelog/45326.txt new file mode 100644 index 000000000000..23582682910f --- /dev/null +++ b/.changelog/45326.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_eks_capability +``` \ No newline at end of file diff --git a/docs/acc-test-environment-variables.md b/docs/acc-test-environment-variables.md index e282f75d07a9..6d50e00f87bb 100644 --- a/docs/acc-test-environment-variables.md +++ b/docs/acc-test-environment-variables.md @@ -60,6 +60,10 @@ Environment variables (beyond standard AWS Go SDK ones) used by acceptance testi | `AWS_EC2_VERIFIED_ACCESS_INSTANCE_LIMIT` | Concurrency limit for Verified Access acceptance tests. [Default is 5](https://docs.aws.amazon.com/verified-access/latest/ug/verified-access-quotas.html) if not specified. | | `AWS_GUARDDUTY_MEMBER_ACCOUNT_ID` | Identifier of AWS Account for GuardDuty Member testing. **DEPRECATED:** Should be replaced with standard alternate account handling for tests. | | `AWS_GUARDDUTY_MEMBER_EMAIL` | Email address for GuardDuty Member testing. **DEPRECATED:** It may be possible to use a placeholder email address instead. | +| `AWS_IDENTITY_STORE_GROUP_ID` | ID of a valid AWS Identity Store group. | +| `AWS_IDENTITY_STORE_GROUP_NAME` | Name of a valid AWS Identity Store group. | +| `AWS_IDENTITY_STORE_USER_ID` | ID of a valid AWS Identity Store user. | +| `AWS_IDENTITY_STORE_USER_NAME` | Name of a valid AWS Identity Store user. | | `AWS_LAMBDA_IMAGE_LATEST_ID` | ECR repository image URI (tagged as `latest`) for Lambda container image acceptance tests. | | `AWS_LAMBDA_IMAGE_V1_ID` | ECR repository image URI (tagged as `v1`) for Lambda container image acceptance tests. | | `AWS_LAMBDA_IMAGE_V2_ID` | ECR repository image URI (tagged as `v2`) for Lambda container image acceptance tests. | diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index 0b6c418f5e8d..ad056114b5de 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -2180,12 +2180,7 @@ func CheckResourceAttrIsJSONString(n, key string) resource.TestCheckFunc { // The variable's value is returned. func SkipIfEnvVarNotSet(t *testing.T, key string) string { t.Helper() - - v := os.Getenv(key) - if v == "" { - t.Skipf("Environment variable %s is not set, skipping test", key) - } - return v + return envvar.SkipIfEmpty(t, key, "") } // SkipIfExeNotOnPath skips the current test if the specified executable is not found in the directories named by the PATH environment variable. diff --git a/internal/envvar/envvar.go b/internal/envvar/envvar.go index f8c9335896be..4cb049c5101e 100644 --- a/internal/envvar/envvar.go +++ b/internal/envvar/envvar.go @@ -172,7 +172,11 @@ func SkipIfEmpty(t testing.T, name string, usageMessage string) string { value := os.Getenv(name) if value == "" { - t.Skipf("skipping test; environment variable %s must be set. Usage: %s", name, usageMessage) + msg := fmt.Sprintf("skipping test; environment variable %s must be set", name) + if usageMessage != "" { + msg += ". Usage: " + usageMessage + } + t.Skip(msg) } return value diff --git a/internal/service/eks/addon.go b/internal/service/eks/addon.go index f48bfde08529..879d595739dd 100644 --- a/internal/service/eks/addon.go +++ b/internal/service/eks/addon.go @@ -411,11 +411,15 @@ func flattenAddonPodIdentityAssociations(ctx context.Context, associations []str } func findAddonByTwoPartKey(ctx context.Context, conn *eks.Client, clusterName, addonName string) (*types.Addon, error) { - input := &eks.DescribeAddonInput{ + input := eks.DescribeAddonInput{ AddonName: aws.String(addonName), ClusterName: aws.String(clusterName), } + return findAddon(ctx, conn, &input) +} + +func findAddon(ctx context.Context, conn *eks.Client, input *eks.DescribeAddonInput) (*types.Addon, error) { output, err := conn.DescribeAddon(ctx, input) if errs.IsA[*types.ResourceNotFoundException](err) { @@ -437,30 +441,13 @@ func findAddonByTwoPartKey(ctx context.Context, conn *eks.Client, clusterName, a } func findAddonUpdateByThreePartKey(ctx context.Context, conn *eks.Client, clusterName, addonName, id string) (*types.Update, error) { - input := &eks.DescribeUpdateInput{ + input := eks.DescribeUpdateInput{ AddonName: aws.String(addonName), Name: aws.String(clusterName), UpdateId: aws.String(id), } - output, err := conn.DescribeUpdate(ctx, input) - - if errs.IsA[*types.ResourceNotFoundException](err) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil || output.Update == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output.Update, nil + return findUpdate(ctx, conn, &input) } func statusAddon(ctx context.Context, conn *eks.Client, clusterName, addonName string) retry.StateRefreshFunc { diff --git a/internal/service/eks/capability.go b/internal/service/eks/capability.go new file mode 100644 index 000000000000..bebe85b17e31 --- /dev/null +++ b/internal/service/eks/capability.go @@ -0,0 +1,633 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package eks + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + awstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + set "github.com/hashicorp/go-set/v3" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + sdkretry "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + intflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + fwvalidators "github.com/hashicorp/terraform-provider-aws/internal/framework/validators" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_eks_capability", name="Capability") +// @Tags(identifierAttribute="arn") +func newCapabilityResource(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &capabilityResource{} + + r.SetDefaultCreateTimeout(20 * time.Minute) + r.SetDefaultUpdateTimeout(20 * time.Minute) + r.SetDefaultDeleteTimeout(20 * time.Minute) + + return r, nil +} + +type capabilityResource struct { + framework.ResourceWithModel[capabilityResourceModel] + framework.WithTimeouts +} + +func (r *capabilityResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + "capability_name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrClusterName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "delete_propagation_policy": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.CapabilityDeletePropagationPolicy](), + Required: true, + }, + names.AttrRoleARN: schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + names.AttrType: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.CapabilityType](), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrVersion: schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + names.AttrConfiguration: schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[capabilityConfigurationModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "argo_cd": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[argoCDConfigModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrNamespace: schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "server_url": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "aws_idc": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[argoCDAWSIDCConfigModel](ctx), + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "idc_instance_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "idc_managed_application_arn": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "idc_region": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + fwvalidators.AWSRegion(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + "network_access": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[argoCDNetworkAccessConfigModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "vpce_ids": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + ElementType: types.StringType, + Optional: true, + }, + }, + }, + }, + "rbac_role_mapping": schema.SetNestedBlock{ + CustomType: fwtypes.NewSetNestedObjectTypeOf[argoCDRoleMappingModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrRole: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ArgoCdRole](), + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "identity": schema.SetNestedBlock{ + CustomType: fwtypes.NewSetNestedObjectTypeOf[SSOIdentity](ctx), + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrID: schema.StringAttribute{ + Required: true, + }, + names.AttrType: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.SsoIdentityType](), + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *capabilityResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data capabilityResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().EKSClient(ctx) + + clusterName, capabilityName := fwflex.StringValueFromFramework(ctx, data.ClusterName), fwflex.StringValueFromFramework(ctx, data.CapabilityName) + var input eks.CreateCapabilityInput + response.Diagnostics.Append(fwflex.Expand(ctx, data, &input)...) + if response.Diagnostics.HasError() { + return + } + + // Additional fields. + input.ClientRequestToken = aws.String(sdkid.UniqueId()) + input.Tags = getTagsIn(ctx) + + _, err := conn.CreateCapability(ctx, &input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("creating EKS Capability (%s,%s)", clusterName, capabilityName), err.Error()) + + return + } + + capability, err := waitCapabilityCreated(ctx, conn, clusterName, capabilityName, r.CreateTimeout(ctx, data.Timeouts)) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for EKS Capability (%s,%s) create", clusterName, capabilityName), err.Error()) + + return + } + + // Set values for unknowns. + response.Diagnostics.Append(fwflex.Flatten(ctx, capability, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, data)...) +} + +func (r *capabilityResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data capabilityResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().EKSClient(ctx) + + clusterName, capabilityName := fwflex.StringValueFromFramework(ctx, data.ClusterName), fwflex.StringValueFromFramework(ctx, data.CapabilityName) + output, err := findCapabilityByTwoPartKey(ctx, conn, clusterName, capabilityName) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading EKS Capability (%s,%s)", clusterName, capabilityName), err.Error()) + + return + } + + // Normalize. + if output.Configuration != nil && output.Configuration.ArgoCd == nil { + output.Configuration = nil + } + + // Set attributes for import. + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + if response.Diagnostics.HasError() { + return + } + + setTagsOut(ctx, output.Tags) + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *capabilityResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new capabilityResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().EKSClient(ctx) + + diff, d := fwflex.Diff(ctx, new, old) + response.Diagnostics.Append(d...) + if response.Diagnostics.HasError() { + return + } + + if diff.HasChanges() { + clusterName, capabilityName := fwflex.StringValueFromFramework(ctx, new.ClusterName), fwflex.StringValueFromFramework(ctx, new.CapabilityName) + var input eks.UpdateCapabilityInput + response.Diagnostics.Append(fwflex.Expand(ctx, new, &input, fwflex.WithIgnoredFieldNamesAppend("RbacRoleMappings"))...) + if response.Diagnostics.HasError() { + return + } + + // Additional fields. + input.ClientRequestToken = aws.String(sdkid.UniqueId()) + + // argo_cd block can only be modified in-place (not added or removed). + var oldConfiguration, newConfiguration awstypes.CapabilityConfigurationRequest + response.Diagnostics.Append(fwflex.Expand(ctx, old.Configuration, &oldConfiguration)...) + if response.Diagnostics.HasError() { + return + } + response.Diagnostics.Append(fwflex.Expand(ctx, new.Configuration, &newConfiguration)...) + if response.Diagnostics.HasError() { + return + } + + if oldArgoCD, newArgoCD := oldConfiguration.ArgoCd, newConfiguration.ArgoCd; oldArgoCD != nil && newArgoCD != nil { + add, remove, update, _ := intflex.DiffSlicesWithModify(oldArgoCD.RbacRoleMappings, newArgoCD.RbacRoleMappings, + func(a, b awstypes.ArgoCdRoleMapping) bool { + hashIdentity := func(v awstypes.SsoIdentity) string { + return string(v.Type) + ":" + aws.ToString(v.Id) + } + return a.Role == b.Role && set.HashSetFromFunc(a.Identities, hashIdentity).Equal(set.HashSetFromFunc(b.Identities, hashIdentity)) + }, func(a, b awstypes.ArgoCdRoleMapping) bool { + return a.Role == b.Role + }) + + input.Configuration.ArgoCd.RbacRoleMappings = &awstypes.UpdateRoleMappings{} + if addOrUpdate := append(add, update...); len(addOrUpdate) > 0 { //nolint:gocritic // append re-assign is intentional + input.Configuration.ArgoCd.RbacRoleMappings.AddOrUpdateRoleMappings = addOrUpdate + } + if len(remove) > 0 { + input.Configuration.ArgoCd.RbacRoleMappings.RemoveRoleMappings = remove + } + } + + output, err := conn.UpdateCapability(ctx, &input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating EKS Capability (%s,%s)", clusterName, capabilityName), err.Error()) + + return + } + + updateID := aws.ToString(output.Update.Id) + if _, err := waitCapabilityUpdateSuccessful(ctx, conn, clusterName, capabilityName, updateID, r.UpdateTimeout(ctx, new.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for EKS Capability (%s,%s) update (%s)", clusterName, capabilityName, updateID), err.Error()) + + return + } + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *capabilityResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data capabilityResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().EKSClient(ctx) + + clusterName, capabilityName := fwflex.StringValueFromFramework(ctx, data.ClusterName), fwflex.StringValueFromFramework(ctx, data.CapabilityName) + input := eks.DeleteCapabilityInput{ + CapabilityName: aws.String(capabilityName), + ClusterName: aws.String(clusterName), + } + _, err := conn.DeleteCapability(ctx, &input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting EKS Capability (%s,%s)", clusterName, capabilityName), err.Error()) + + return + } + + if _, err := waitCapabilityDeleted(ctx, conn, clusterName, capabilityName, r.DeleteTimeout(ctx, data.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for EKS Capability (%s,%s) delete", clusterName, capabilityName), err.Error()) + + return + } +} + +func (r *capabilityResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + const ( + capability = 2 + ) + parts, err := intflex.ExpandResourceId(request.ID, capability, true) + + if err != nil { + response.Diagnostics.Append(fwdiag.NewParsingResourceIDErrorDiagnostic(err)) + + return + } + + response.State.SetAttribute(ctx, path.Root(names.AttrClusterName), parts[0]) + response.State.SetAttribute(ctx, path.Root("capability_name"), parts[1]) +} + +func findCapabilityByTwoPartKey(ctx context.Context, conn *eks.Client, clusterName, capabilityName string) (*awstypes.Capability, error) { + input := eks.DescribeCapabilityInput{ + CapabilityName: aws.String(capabilityName), + ClusterName: aws.String(clusterName), + } + + return findCapability(ctx, conn, &input) +} + +func findCapability(ctx context.Context, conn *eks.Client, input *eks.DescribeCapabilityInput) (*awstypes.Capability, error) { + output, err := conn.DescribeCapability(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &sdkretry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Capability == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Capability, nil +} + +func findCapabilityUpdateByThreePartKey(ctx context.Context, conn *eks.Client, clusterName, capabilityName, id string) (*awstypes.Update, error) { + input := eks.DescribeUpdateInput{ + CapabilityName: aws.String(capabilityName), + Name: aws.String(clusterName), + UpdateId: aws.String(id), + } + + return findUpdate(ctx, conn, &input) +} + +func statusCapability(ctx context.Context, conn *eks.Client, clusterName, capabilityName string) sdkretry.StateRefreshFunc { + return func() (any, string, error) { + output, err := findCapabilityByTwoPartKey(ctx, conn, clusterName, capabilityName) + + if retry.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} + +func statusCapabilityUpdate(ctx context.Context, conn *eks.Client, clusterName, capabilityName, id string) sdkretry.StateRefreshFunc { + return func() (any, string, error) { + output, err := findCapabilityUpdateByThreePartKey(ctx, conn, clusterName, capabilityName, id) + + if retry.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} + +func waitCapabilityCreated(ctx context.Context, conn *eks.Client, clusterName, capabilityName string, timeout time.Duration) (*awstypes.Capability, error) { + stateConf := sdkretry.StateChangeConf{ + Pending: enum.Slice(awstypes.CapabilityStatusCreating), + Target: enum.Slice(awstypes.CapabilityStatusActive), + Refresh: statusCapability(ctx, conn, clusterName, capabilityName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.Capability); ok { + if status, health := output.Status, output.Health; status == awstypes.CapabilityStatusCreateFailed && health != nil { + tfresource.SetLastError(err, capabilityIssuesError(health.Issues)) + } + + return output, err + } + + return nil, err +} + +func waitCapabilityDeleted(ctx context.Context, conn *eks.Client, clusterName, capabilityName string, timeout time.Duration) (*awstypes.Capability, error) { + stateConf := &sdkretry.StateChangeConf{ + Pending: enum.Slice(awstypes.CapabilityStatusActive, awstypes.CapabilityStatusDeleting), + Target: []string{}, + Refresh: statusCapability(ctx, conn, clusterName, capabilityName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.Capability); ok { + if status, health := output.Status, output.Health; status == awstypes.CapabilityStatusDeleteFailed && health != nil { + tfresource.SetLastError(err, capabilityIssuesError(health.Issues)) + } + + return output, err + } + + return nil, err +} + +func waitCapabilityUpdateSuccessful(ctx context.Context, conn *eks.Client, clusterName, capabilityName, id string, timeout time.Duration) (*awstypes.Update, error) { + stateConf := sdkretry.StateChangeConf{ + Pending: enum.Slice(awstypes.UpdateStatusInProgress), + Target: enum.Slice(awstypes.UpdateStatusSuccessful), + Refresh: statusCapabilityUpdate(ctx, conn, clusterName, capabilityName, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.Update); ok { + if status := output.Status; status == awstypes.UpdateStatusCancelled || status == awstypes.UpdateStatusFailed { + tfresource.SetLastError(err, errorDetailsError(output.Errors)) + } + + return output, err + } + + return nil, err +} + +func capabilityIssueError(apiObject awstypes.CapabilityIssue) error { + return fmt.Errorf("%s: %s", apiObject.Code, aws.ToString(apiObject.Message)) +} + +func capabilityIssuesError(apiObjects []awstypes.CapabilityIssue) error { + var errs []error + + for _, apiObject := range apiObjects { + errs = append(errs, capabilityIssueError(apiObject)) + } + + return errors.Join(errs...) +} + +type capabilityResourceModel struct { + framework.WithRegionModel + ARN types.String `tfsdk:"arn"` + CapabilityName types.String `tfsdk:"capability_name"` + ClusterName types.String `tfsdk:"cluster_name"` + Configuration fwtypes.ListNestedObjectValueOf[capabilityConfigurationModel] `tfsdk:"configuration"` + DeletePropagationPolicy fwtypes.StringEnum[awstypes.CapabilityDeletePropagationPolicy] `tfsdk:"delete_propagation_policy"` + RoleARN fwtypes.ARN `tfsdk:"role_arn"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + Type fwtypes.StringEnum[awstypes.CapabilityType] `tfsdk:"type"` + Version types.String `tfsdk:"version"` +} + +type capabilityConfigurationModel struct { + ArgoCD fwtypes.ListNestedObjectValueOf[argoCDConfigModel] `tfsdk:"argo_cd"` +} + +type argoCDConfigModel struct { + AWSIDC fwtypes.ListNestedObjectValueOf[argoCDAWSIDCConfigModel] `tfsdk:"aws_idc"` + Namespace types.String `tfsdk:"namespace"` + NetworkAccess fwtypes.ListNestedObjectValueOf[argoCDNetworkAccessConfigModel] `tfsdk:"network_access"` + RBACRoleMappings fwtypes.SetNestedObjectValueOf[argoCDRoleMappingModel] `tfsdk:"rbac_role_mapping"` + ServerURL types.String `tfsdk:"server_url"` +} + +type argoCDAWSIDCConfigModel struct { + IDCInstanceARN fwtypes.ARN `tfsdk:"idc_instance_arn"` + IDCManagedApplicationARN fwtypes.ARN `tfsdk:"idc_managed_application_arn"` + IDCRegion types.String `tfsdk:"idc_region"` +} + +type argoCDNetworkAccessConfigModel struct { + VPCEIDs fwtypes.SetOfString `tfsdk:"vpce_ids"` +} + +type argoCDRoleMappingModel struct { + Identities fwtypes.SetNestedObjectValueOf[SSOIdentity] `tfsdk:"identity"` + Role fwtypes.StringEnum[awstypes.ArgoCdRole] `tfsdk:"role"` +} + +type SSOIdentity struct { + ID types.String `tfsdk:"id"` + Type fwtypes.StringEnum[awstypes.SsoIdentityType] `tfsdk:"type"` +} diff --git a/internal/service/eks/capability_test.go b/internal/service/eks/capability_test.go new file mode 100644 index 000000000000..d92af40abb68 --- /dev/null +++ b/internal/service/eks/capability_test.go @@ -0,0 +1,509 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package eks_test + +import ( + "context" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/service/eks/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfknownvalue "github.com/hashicorp/terraform-provider-aws/internal/acctest/knownvalue" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfeks "github.com/hashicorp/terraform-provider-aws/internal/service/eks" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccEKSCapability_basic(t *testing.T) { + ctx := acctest.Context(t) + var capability types.Capability + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_eks_capability.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EKSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapabilityDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCapabilityConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapabilityExists(ctx, resourceName, &capability), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), tfknownvalue.RegionalARNRegexp("eks", regexache.MustCompile(`capability/.+`))), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("capability_name"), knownvalue.StringExact(rName)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, ",", names.AttrClusterName, "capability_name"), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccEKSCapability_disappears(t *testing.T) { + ctx := acctest.Context(t) + var capability types.Capability + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_eks_capability.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EKSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapabilityDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCapabilityConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapabilityExists(ctx, resourceName, &capability), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfeks.ResourceCapability, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func TestAccEKSCapability_tags(t *testing.T) { + ctx := acctest.Context(t) + var capability types.Capability + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_eks_capability.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EKSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapabilityDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCapabilityConfig_tags1(rName, acctest.CtKey1, acctest.CtValue1), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapabilityExists(ctx, resourceName, &capability), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{ + acctest.CtKey1: knownvalue.StringExact(acctest.CtValue1), + })), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, ",", names.AttrClusterName, "capability_name"), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccCapabilityConfig_tags2(rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapabilityExists(ctx, resourceName, &capability), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{ + acctest.CtKey1: knownvalue.StringExact(acctest.CtValue1Updated), + acctest.CtKey2: knownvalue.StringExact(acctest.CtValue2), + })), + }, + }, + { + Config: testAccCapabilityConfig_tags1(rName, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapabilityExists(ctx, resourceName, &capability), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{ + acctest.CtKey2: knownvalue.StringExact(acctest.CtValue2), + })), + }, + }, + }, + }) +} + +func TestAccEKSCapability_ArgoCD_basic(t *testing.T) { + ctx := acctest.Context(t) + var capability types.Capability + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_eks_capability.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + acctest.PreCheckSSOAdminInstances(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.EKSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapabilityDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCapabilityConfig_argoCDBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapabilityExists(ctx, resourceName, &capability), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, ",", names.AttrClusterName, "capability_name"), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccEKSCapability_ArgoCD_rbac(t *testing.T) { + ctx := acctest.Context(t) + var capability types.Capability + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_eks_capability.test" + userID := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_ID") + groupID := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_GROUP_ID") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + acctest.PreCheckSSOAdminInstances(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.EKSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapabilityDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCapabilityConfig_argoCDRBAC1(rName, userID, groupID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapabilityExists(ctx, resourceName, &capability), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, ",", names.AttrClusterName, "capability_name"), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccCapabilityConfig_argoCDRBAC2(rName, userID, groupID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapabilityExists(ctx, resourceName, &capability), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + }, + }) +} + +func testAccCheckCapabilityExists(ctx context.Context, n string, v *types.Capability) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EKSClient(ctx) + + output, err := tfeks.FindCapabilityByTwoPartKey(ctx, conn, rs.Primary.Attributes[names.AttrClusterName], rs.Primary.Attributes["capability_name"]) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckCapabilityDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EKSClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_eks_capability" { + continue + } + + _, err := tfeks.FindCapabilityByTwoPartKey(ctx, conn, rs.Primary.Attributes[names.AttrClusterName], rs.Primary.Attributes["capability_name"]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("EKS Capability (%s,%s) still exists", rs.Primary.Attributes[names.AttrClusterName], rs.Primary.Attributes["capability_name"]) + } + + return nil + } +} + +func testAccCapabilityConfig_base(rName string) string { + return acctest.ConfigCompose(testAccClusterConfig_base(rName), fmt.Sprintf(` +resource "aws_eks_cluster" "test" { + name = %[1]q + role_arn = aws_iam_role.cluster.arn + + access_config { + authentication_mode = "API" + bootstrap_cluster_creator_admin_permissions = true + } + + vpc_config { + subnet_ids = aws_subnet.test[*].id + } + + depends_on = [aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy] +} + +resource "aws_iam_role" "capability" { + name = "%[1]s-capability" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + Service = "capabilities.eks.amazonaws.com" + } + Action = [ + "sts:AssumeRole", + "sts:TagSession" + ] + }] + }) +} + +resource "aws_iam_role_policy_attachment" "capability" { + role = aws_iam_role.capability.name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AdministratorAccess" +} +`, rName)) +} + +func testAccCapabilityConfig_basic(rName string) string { + return acctest.ConfigCompose(testAccCapabilityConfig_base(rName), fmt.Sprintf(` +resource "aws_eks_capability" "test" { + cluster_name = aws_eks_cluster.test.name + capability_name = %[1]q + type = "KRO" + role_arn = aws_iam_role.capability.arn + delete_propagation_policy = "RETAIN" + + depends_on = [aws_iam_role_policy_attachment.capability] +} +`, rName)) +} + +func testAccCapabilityConfig_tags1(rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose(testAccCapabilityConfig_base(rName), fmt.Sprintf(` +resource "aws_eks_capability" "test" { + cluster_name = aws_eks_cluster.test.name + capability_name = %[1]q + type = "KRO" + role_arn = aws_iam_role.capability.arn + delete_propagation_policy = "RETAIN" + + tags = { + %[2]q = %[3]q + } + + depends_on = [aws_iam_role_policy_attachment.capability] +} +`, rName, tagKey1, tagValue1)) +} + +func testAccCapabilityConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose(testAccCapabilityConfig_base(rName), fmt.Sprintf(` +resource "aws_eks_capability" "test" { + cluster_name = aws_eks_cluster.test.name + capability_name = %[1]q + type = "KRO" + role_arn = aws_iam_role.capability.arn + delete_propagation_policy = "RETAIN" + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } + + depends_on = [aws_iam_role_policy_attachment.capability] +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} + +func testAccCapabilityConfig_argoCDBasic(rName string) string { + return acctest.ConfigCompose(testAccCapabilityConfig_base(rName), fmt.Sprintf(` +data "aws_ssoadmin_instances" "test" {} + +resource "aws_eks_capability" "test" { + cluster_name = aws_eks_cluster.test.name + capability_name = %[1]q + type = "ARGOCD" + role_arn = aws_iam_role.capability.arn + delete_propagation_policy = "RETAIN" + + configuration { + argo_cd { + aws_idc { + idc_instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + } + } + } + + depends_on = [aws_iam_role_policy_attachment.capability] +} +`, rName)) +} + +func testAccCapabilityConfig_argoCDRBAC1(rName, userID, groupName string) string { + return acctest.ConfigCompose(testAccCapabilityConfig_base(rName), fmt.Sprintf(` +data "aws_ssoadmin_instances" "test" {} + +resource "aws_eks_capability" "test" { + cluster_name = aws_eks_cluster.test.name + capability_name = %[1]q + type = "ARGOCD" + role_arn = aws_iam_role.capability.arn + delete_propagation_policy = "RETAIN" + + configuration { + argo_cd { + aws_idc { + idc_instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + idc_region = %[2]q + } + + rbac_role_mapping { + role = "ADMIN" + + identity { + type = "SSO_USER" + id = %[3]q + } + } + + rbac_role_mapping { + role = "VIEWER" + + identity { + type = "SSO_GROUP" + id = %[4]q + } + } + } + } + + depends_on = [aws_iam_role_policy_attachment.capability] +} +`, rName, acctest.Region(), userID, groupName)) +} + +func testAccCapabilityConfig_argoCDRBAC2(rName, userID, groupName string) string { + return acctest.ConfigCompose(testAccCapabilityConfig_base(rName), fmt.Sprintf(` +data "aws_ssoadmin_instances" "test" {} + +resource "aws_eks_capability" "test" { + cluster_name = aws_eks_cluster.test.name + capability_name = %[1]q + type = "ARGOCD" + role_arn = aws_iam_role.capability.arn + delete_propagation_policy = "RETAIN" + + configuration { + argo_cd { + aws_idc { + idc_instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + idc_region = %[2]q + } + + rbac_role_mapping { + role = "EDITOR" + + identity { + type = "SSO_USER" + id = %[3]q + } + } + + rbac_role_mapping { + role = "ADMIN" + + identity { + type = "SSO_GROUP" + id = %[4]q + } + } + } + } + + depends_on = [aws_iam_role_policy_attachment.capability] +} +`, rName, acctest.Region(), userID, groupName)) +} diff --git a/internal/service/eks/cluster.go b/internal/service/eks/cluster.go index 53888c433039..783e5735efbb 100644 --- a/internal/service/eks/cluster.go +++ b/internal/service/eks/cluster.go @@ -1044,7 +1044,7 @@ func updateClusterVPCConfig(ctx context.Context, conn *eks.Client, name string, return nil } -func findUpdateByTwoPartKey(ctx context.Context, conn *eks.Client, name, id string) (*types.Update, error) { +func findClusterUpdateByTwoPartKey(ctx context.Context, conn *eks.Client, name, id string) (*types.Update, error) { input := eks.DescribeUpdateInput{ Name: aws.String(name), UpdateId: aws.String(id), @@ -1092,7 +1092,7 @@ func statusCluster(ctx context.Context, conn *eks.Client, name string) retry.Sta func statusUpdate(ctx context.Context, conn *eks.Client, name, id string) retry.StateRefreshFunc { return func() (any, string, error) { - output, err := findUpdateByTwoPartKey(ctx, conn, name, id) + output, err := findClusterUpdateByTwoPartKey(ctx, conn, name, id) if tfresource.NotFound(err) { return nil, "", nil diff --git a/internal/service/eks/exports_test.go b/internal/service/eks/exports_test.go index 81bdfea5dc99..e31f402cff7c 100644 --- a/internal/service/eks/exports_test.go +++ b/internal/service/eks/exports_test.go @@ -8,6 +8,7 @@ var ( ResourceAccessEntry = resourceAccessEntry ResourceAccessPolicyAssociation = resourceAccessPolicyAssociation ResourceAddon = resourceAddon + ResourceCapability = newCapabilityResource ResourceCluster = resourceCluster ResourceFargateProfile = resourceFargateProfile ResourceIdentityProviderConfig = resourceIdentityProviderConfig @@ -18,6 +19,7 @@ var ( FindAccessEntryByTwoPartKey = findAccessEntryByTwoPartKey FindAccessPolicyAssociationByThreePartKey = findAccessPolicyAssociationByThreePartKey FindAddonByTwoPartKey = findAddonByTwoPartKey + FindCapabilityByTwoPartKey = findCapabilityByTwoPartKey FindClusterByName = findClusterByName FindFargateProfileByTwoPartKey = findFargateProfileByTwoPartKey FindNodegroupByTwoPartKey = findNodegroupByTwoPartKey diff --git a/internal/service/eks/node_group.go b/internal/service/eks/node_group.go index 01c5189b16d8..8e43d8f9da81 100644 --- a/internal/service/eks/node_group.go +++ b/internal/service/eks/node_group.go @@ -668,11 +668,15 @@ func resourceNodeGroupDelete(ctx context.Context, d *schema.ResourceData, meta a } func findNodegroupByTwoPartKey(ctx context.Context, conn *eks.Client, clusterName, nodeGroupName string) (*types.Nodegroup, error) { - input := &eks.DescribeNodegroupInput{ + input := eks.DescribeNodegroupInput{ ClusterName: aws.String(clusterName), NodegroupName: aws.String(nodeGroupName), } + return findNodegroup(ctx, conn, &input) +} + +func findNodegroup(ctx context.Context, conn *eks.Client, input *eks.DescribeNodegroupInput) (*types.Nodegroup, error) { output, err := conn.DescribeNodegroup(ctx, input) if errs.IsA[*types.ResourceNotFoundException](err) { @@ -694,30 +698,13 @@ func findNodegroupByTwoPartKey(ctx context.Context, conn *eks.Client, clusterNam } func findNodegroupUpdateByThreePartKey(ctx context.Context, conn *eks.Client, clusterName, nodeGroupName, id string) (*types.Update, error) { - input := &eks.DescribeUpdateInput{ + input := eks.DescribeUpdateInput{ Name: aws.String(clusterName), NodegroupName: aws.String(nodeGroupName), UpdateId: aws.String(id), } - output, err := conn.DescribeUpdate(ctx, input) - - if errs.IsA[*types.ResourceNotFoundException](err) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil || output.Update == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output.Update, nil + return findUpdate(ctx, conn, &input) } func statusNodegroup(ctx context.Context, conn *eks.Client, clusterName, nodeGroupName string) retry.StateRefreshFunc { diff --git a/internal/service/eks/service_package_gen.go b/internal/service/eks/service_package_gen.go index be71f63e85ba..741578cfe138 100644 --- a/internal/service/eks/service_package_gen.go +++ b/internal/service/eks/service_package_gen.go @@ -41,6 +41,15 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*inttypes.S func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.ServicePackageFrameworkResource { return []*inttypes.ServicePackageFrameworkResource{ + { + Factory: newCapabilityResource, + TypeName: "aws_eks_capability", + Name: "Capability", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDefault()), + }, { Factory: newPodIdentityAssociationResource, TypeName: "aws_eks_pod_identity_association", diff --git a/internal/service/emr/studio_session_mapping_test.go b/internal/service/emr/studio_session_mapping_test.go index 9a9ddaa9b50d..c2bcce1d6f63 100644 --- a/internal/service/emr/studio_session_mapping_test.go +++ b/internal/service/emr/studio_session_mapping_test.go @@ -6,7 +6,6 @@ package emr_test import ( "context" "fmt" - "os" "testing" awstypes "github.com/aws/aws-sdk-go-v2/service/emr/types" @@ -26,15 +25,11 @@ func TestAccEMRStudioSessionMapping_basic(t *testing.T) { resourceName := "aws_emr_studio_session_mapping.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) updatedName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - uName := os.Getenv("AWS_IDENTITY_STORE_USER_ID") - gName := os.Getenv("AWS_IDENTITY_STORE_GROUP_NAME") + uName := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_ID") + gName := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_GROUP_NAME") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - acctest.PreCheck(ctx, t) - testAccPreCheckUserID(t) - testAccPreCheckGroupName(t) - }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.EMRServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckStudioSessionMappingDestroy(ctx), @@ -93,15 +88,11 @@ func TestAccEMRStudioSessionMapping_disappears(t *testing.T) { var studio awstypes.SessionMappingDetail resourceName := "aws_emr_studio_session_mapping.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - uName := os.Getenv("AWS_IDENTITY_STORE_USER_ID") - gName := os.Getenv("AWS_IDENTITY_STORE_GROUP_NAME") + uName := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_ID") + gName := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_GROUP_NAME") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - acctest.PreCheck(ctx, t) - testAccPreCheckUserID(t) - testAccPreCheckGroupName(t) - }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.EMRServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckStudioSessionMappingDestroy(ctx), @@ -175,20 +166,6 @@ func testAccCheckStudioSessionMappingDestroy(ctx context.Context) resource.TestC } } -func testAccPreCheckUserID(t *testing.T) { - if os.Getenv("AWS_IDENTITY_STORE_USER_ID") == "" { - t.Skip("AWS_IDENTITY_STORE_USER_ID env var must be set for AWS Identity Store User acceptance test. " + - "This is required until ListUsers API returns results without filtering by name.") - } -} - -func testAccPreCheckGroupName(t *testing.T) { - if os.Getenv("AWS_IDENTITY_STORE_GROUP_NAME") == "" { - t.Skip("AWS_IDENTITY_STORE_GROUP_NAME env var must be set for AWS Identity Store Group acceptance test. " + - "This is required until ListGroups API returns results without filtering by name.") - } -} - func testAccStudioSessionMappingConfigBase(rName string) string { return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` data "aws_partition" "current" {} diff --git a/internal/service/kendra/experience_test.go b/internal/service/kendra/experience_test.go index 99ad5a37cc43..e9a893f6528e 100644 --- a/internal/service/kendra/experience_test.go +++ b/internal/service/kendra/experience_test.go @@ -6,7 +6,6 @@ package kendra_test import ( "context" "fmt" - "os" "testing" "github.com/YakDriver/regexache" @@ -395,11 +394,7 @@ func TestAccKendraExperience_Configuration_UserIdentityConfiguration(t *testing. t.Skip("skipping long-running test in short mode") } - userId := os.Getenv("AWS_IDENTITY_STORE_USER_ID") - if userId == "" { - t.Skip("Environment variable AWS_IDENTITY_STORE_USER_ID is not set") - } - + userId := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_ID") rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_kendra_experience.test" @@ -437,11 +432,7 @@ func TestAccKendraExperience_Configuration_ContentSourceConfigurationAndUserIden t.Skip("skipping long-running test in short mode") } - userId := os.Getenv("AWS_IDENTITY_STORE_USER_ID") - if userId == "" { - t.Skip("Environment variable AWS_IDENTITY_STORE_USER_ID is not set") - } - + userId := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_ID") rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_kendra_experience.test" @@ -481,11 +472,7 @@ func TestAccKendraExperience_Configuration_ContentSourceConfigurationWithUserIde t.Skip("skipping long-running test in short mode") } - userId := os.Getenv("AWS_IDENTITY_STORE_USER_ID") - if userId == "" { - t.Skip("Environment variable AWS_IDENTITY_STORE_USER_ID is not set") - } - + userId := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_ID") rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_kendra_experience.test" @@ -535,11 +522,7 @@ func TestAccKendraExperience_Configuration_UserIdentityConfigurationWithContentS t.Skip("skipping long-running test in short mode") } - userId := os.Getenv("AWS_IDENTITY_STORE_USER_ID") - if userId == "" { - t.Skip("Environment variable AWS_IDENTITY_STORE_USER_ID is not set") - } - + userId := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_ID") rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_kendra_experience.test" diff --git a/internal/service/ssoadmin/account_assignment_test.go b/internal/service/ssoadmin/account_assignment_test.go index 3b94087474a9..0a798be66cd9 100644 --- a/internal/service/ssoadmin/account_assignment_test.go +++ b/internal/service/ssoadmin/account_assignment_test.go @@ -6,7 +6,6 @@ package ssoadmin_test import ( "context" "fmt" - "os" "testing" "github.com/YakDriver/regexache" @@ -24,13 +23,12 @@ func TestAccSSOAdminAccountAssignment_Basic_group(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_ssoadmin_account_assignment.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - groupName := os.Getenv("AWS_IDENTITY_STORE_GROUP_NAME") + groupName := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_GROUP_NAME") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckSSOAdminInstances(ctx, t) - testAccPreCheckIdentityStoreGroupName(t) }, ErrorCheck: acctest.ErrorCheck(t, names.SSOAdminServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -58,20 +56,19 @@ func TestAccSSOAdminAccountAssignment_Basic_user(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_ssoadmin_account_assignment.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - userName := os.Getenv("AWS_IDENTITY_STORE_USER_NAME") + userId := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_NAME") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckSSOAdminInstances(ctx, t) - testAccPreCheckIdentityStoreUserName(t) }, ErrorCheck: acctest.ErrorCheck(t, names.SSOAdminServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckAccountAssignmentDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccAccountAssignmentConfig_basicUser(userName, rName), + Config: testAccAccountAssignmentConfig_basicUser(userId, rName), Check: resource.ComposeTestCheckFunc( testAccCheckAccountAssignmentExists(ctx, resourceName), resource.TestCheckResourceAttr(resourceName, "target_type", "AWS_ACCOUNT"), @@ -91,13 +88,12 @@ func TestAccSSOAdminAccountAssignment_Basic_user(t *testing.T) { func TestAccSSOAdminAccountAssignment_MissingPolicy(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - userName := os.Getenv("AWS_IDENTITY_STORE_USER_NAME") + userName := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_USER_NAME") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckSSOAdminInstances(ctx, t) - testAccPreCheckIdentityStoreUserName(t) }, ErrorCheck: acctest.ErrorCheck(t, names.SSOAdminServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -116,13 +112,12 @@ func TestAccSSOAdminAccountAssignment_disappears(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_ssoadmin_account_assignment.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - groupName := os.Getenv("AWS_IDENTITY_STORE_GROUP_NAME") + groupName := acctest.SkipIfEnvVarNotSet(t, "AWS_IDENTITY_STORE_GROUP_NAME") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckSSOAdminInstances(ctx, t) - testAccPreCheckIdentityStoreGroupName(t) }, ErrorCheck: acctest.ErrorCheck(t, names.SSOAdminServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -264,20 +259,6 @@ resource "aws_ssoadmin_account_assignment" "test" { `, userName)) } -func testAccPreCheckIdentityStoreGroupName(t *testing.T) { - if os.Getenv("AWS_IDENTITY_STORE_GROUP_NAME") == "" { - t.Skip("AWS_IDENTITY_STORE_GROUP_NAME env var must be set for AWS Identity Store Group acceptance test. " + - "This is required until ListGroups API returns results without filtering by name.") - } -} - -func testAccPreCheckIdentityStoreUserName(t *testing.T) { - if os.Getenv("AWS_IDENTITY_STORE_USER_NAME") == "" { - t.Skip("AWS_IDENTITY_STORE_USER_NAME env var must be set for AWS Identity Store User acceptance test. " + - "This is required until ListUsers API returns results without filtering by name.") - } -} - func testAccAccountAssignmentConfig_withCustomerPolicy(userName, policyPath, policyName, rName string) string { return acctest.ConfigCompose( testAccAccountAssignmentConfig_basicUser(userName, rName), diff --git a/website/docs/r/eks_capability.html.markdown b/website/docs/r/eks_capability.html.markdown new file mode 100644 index 000000000000..91193e60aa82 --- /dev/null +++ b/website/docs/r/eks_capability.html.markdown @@ -0,0 +1,125 @@ +--- +subcategory: "EKS (Elastic Kubernetes)" +layout: "aws" +page_title: "AWS: aws_eks_capability" +description: |- + Manages an EKS Capability. +--- + +# Resource: aws_eks_capability + +Manages an EKS Capability for an EKS cluster. + +## Example Usage + +```terraform +resource "aws_eks_capability" "example" { + cluster_name = aws_eks_cluster.example.name + capability_name = "argocd" + type = "ARGOCD" + role_arn = aws_iam_role.example.arn + delete_propagation_policy = "RETAIN" + + configuration { + argo_cd { + aws_idc { + idc_instance_arn = "arn:aws:sso:::instance/ssoins-1234567890abcdef0" + } + namespace = "argocd" + } + } + + tags = { + Name = "example-capability" + } +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `capability_name` - (Required) Name of the capability. Must be unique within the cluster. +* `cluster_name` - (Required) Name of the EKS cluster. +* `configuration` - (Optional) Configuration for the capability. See [`configuration`](#configuration) below. +* `delete_propagation_policy` - (Required) Delete propagation policy for the capability. Valid values: `RETAIN`. +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). +* `role_arn` - (Required) ARN of the IAM role to associate with the capability. +* `tags` - (Optional) Key-value map of resource tags. +* `type` - (Required) Type of the capability. Valid values: `ACK`, `KRO`, `ARGOCD`. + +### `configuration` + +The `configuration` block contains the following: + +* `argo_cd` - (Optional) ArgoCD configuration. See [`argo_cd`](#argo_cd) below. + +### `argo_cd` + +The `argo_cd` block contains the following: + +* `aws_idc` - (Optional) AWS IAM Identity Center configuration. See [`aws_idc`](#aws_idc) below. +* `namespace` - (Optional) Kubernetes namespace for ArgoCD. +* `network_access` - (Optional) Network access configuration. See [`network_access`](#network_access) below. +* `rbac_role_mapping` - (Optional) RBAC role mappings. See [`rbac_role_mapping`](#rbac_role_mapping) below. + +### `aws_idc` + +The `aws_idc` block contains the following: + +* `idc_instance_arn` - (Required) ARN of the IAM Identity Center instance. +* `idc_region` - (Optional) Region of the IAM Identity Center instance. + +### `network_access` + +The `network_access` block contains the following: + +* `vpce_ids` - (Optional) VPC Endpoint IDs. + +### `rbac_role_mapping` + +The `rbac_role_mapping` block contains the following: + +* `identity` - (Required) List of identities. See [`identity`](#identity) below. +* `role` - (Required) ArgoCD role. Valid values: `ADMIN`, `EDITOR`, `VIEWER`. + +### `identity` + +The `identity` block contains the following: + +* `id` - (Required) Identity ID. +* `type` - (Required) Identity type. Valid values: `SSO_USER`, `SSO_GROUP`. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the capability. +* `configuration.0.argo_cd.0.server_url` - URL of the Argo CD server. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). +* `version` - Version of the capability. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `20m`) +* `update` - (Default `20m`) +* `delete` - (Default `20m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import EKS Capability using the `cluster_name` and `capability_name` separated by a comma (`,`). For example: + +```terraform +import { + to = aws_eks_capability.example + id = "my-cluster,my-capability" +} +``` + +Using `terraform import`, import EKS Capability using the `cluster_name` and `capability_name` separated by a comma (`,`). For example: + +```console +% terraform import aws_eks_capability.example my-cluster,my-capability +```