diff --git a/.changelog/17959.txt b/.changelog/17959.txt new file mode 100644 index 00000000000..9b0b4cd67b6 --- /dev/null +++ b/.changelog/17959.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_eks_identity_provider_config +``` \ No newline at end of file diff --git a/aws/internal/service/eks/enum.go b/aws/internal/service/eks/enum.go index 8e0f2c21f43..e21b72d590b 100644 --- a/aws/internal/service/eks/enum.go +++ b/aws/internal/service/eks/enum.go @@ -1,5 +1,9 @@ package eks +const ( + IdentityProviderConfigTypeOidc = "oidc" +) + const ( ResourcesSecrets = "secrets" ) diff --git a/aws/internal/service/eks/finder/finder.go b/aws/internal/service/eks/finder/finder.go index 8aef2b0ba6c..3334e44c6eb 100644 --- a/aws/internal/service/eks/finder/finder.go +++ b/aws/internal/service/eks/finder/finder.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/service/eks" "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfeks "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks" ) func AddonByClusterNameAndAddonName(ctx context.Context, conn *eks.EKS, clusterName, addonName string) (*eks.Addon, error) { @@ -214,3 +215,35 @@ func NodegroupUpdateByClusterNameNodegroupNameAndID(conn *eks.EKS, clusterName, return output.Update, nil } + +func OidcIdentityProviderConfigByClusterNameAndConfigName(ctx context.Context, conn *eks.EKS, clusterName, configName string) (*eks.OidcIdentityProviderConfig, error) { + input := &eks.DescribeIdentityProviderConfigInput{ + ClusterName: aws.String(clusterName), + IdentityProviderConfig: &eks.IdentityProviderConfig{ + Name: aws.String(configName), + Type: aws.String(tfeks.IdentityProviderConfigTypeOidc), + }, + } + + output, err := conn.DescribeIdentityProviderConfigWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.IdentityProviderConfig == nil || output.IdentityProviderConfig.Oidc == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.IdentityProviderConfig.Oidc, nil +} diff --git a/aws/internal/service/eks/id.go b/aws/internal/service/eks/id.go index 8aec4cf9a3e..66c86d64104 100644 --- a/aws/internal/service/eks/id.go +++ b/aws/internal/service/eks/id.go @@ -43,6 +43,25 @@ func FargateProfileParseResourceID(id string) (string, string, error) { return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected cluster-name%[2]sfargate-profile-name", id, fargateProfileResourceIDSeparator) } +const identityProviderConfigResourceIDSeparator = ":" + +func IdentityProviderConfigCreateResourceID(clusterName, configName string) string { + parts := []string{clusterName, configName} + id := strings.Join(parts, identityProviderConfigResourceIDSeparator) + + return id +} + +func IdentityProviderConfigParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, identityProviderConfigResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected cluster-name%[2]sconfig-name", id, identityProviderConfigResourceIDSeparator) +} + const nodeGroupResourceIDSeparator = ":" func NodeGroupCreateResourceID(clusterName, nodeGroupName string) string { diff --git a/aws/internal/service/eks/waiter/status.go b/aws/internal/service/eks/waiter/status.go index 96a1a3daa46..c0b4920ef1c 100644 --- a/aws/internal/service/eks/waiter/status.go +++ b/aws/internal/service/eks/waiter/status.go @@ -121,3 +121,19 @@ func NodegroupUpdateStatus(conn *eks.EKS, clusterName, nodeGroupName, id string) return output, aws.StringValue(output.Status), nil } } + +func OidcIdentityProviderConfigStatus(ctx context.Context, conn *eks.EKS, clusterName, configName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.OidcIdentityProviderConfigByClusterNameAndConfigName(ctx, conn, clusterName, configName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/eks/waiter/waiter.go b/aws/internal/service/eks/waiter/waiter.go index 171f55c27fe..2a3289dacd8 100644 --- a/aws/internal/service/eks/waiter/waiter.go +++ b/aws/internal/service/eks/waiter/waiter.go @@ -240,3 +240,37 @@ func NodegroupUpdateSuccessful(conn *eks.EKS, clusterName, nodeGroupName, id str return nil, err } + +func OidcIdentityProviderConfigCreated(ctx context.Context, conn *eks.EKS, clusterName, configName string, timeout time.Duration) (*eks.OidcIdentityProviderConfig, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{eks.ConfigStatusCreating}, + Target: []string{eks.ConfigStatusActive}, + Refresh: OidcIdentityProviderConfigStatus(ctx, conn, clusterName, configName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*eks.OidcIdentityProviderConfig); ok { + return output, err + } + + return nil, err +} + +func OidcIdentityProviderConfigDeleted(ctx context.Context, conn *eks.EKS, clusterName, configName string, timeout time.Duration) (*eks.OidcIdentityProviderConfig, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{eks.ConfigStatusActive, eks.ConfigStatusDeleting}, + Target: []string{}, + Refresh: OidcIdentityProviderConfigStatus(ctx, conn, clusterName, configName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*eks.OidcIdentityProviderConfig); ok { + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index 284c667ae22..8364ac2b7cc 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -729,6 +729,7 @@ func Provider() *schema.Provider { "aws_eks_cluster": resourceAwsEksCluster(), "aws_eks_addon": resourceAwsEksAddon(), "aws_eks_fargate_profile": resourceAwsEksFargateProfile(), + "aws_eks_identity_provider_config": resourceAwsEksIdentityProviderConfig(), "aws_eks_node_group": resourceAwsEksNodeGroup(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), "aws_elasticache_global_replication_group": resourceAwsElasticacheGlobalReplicationGroup(), diff --git a/aws/resource_aws_eks_identity_provider_config.go b/aws/resource_aws_eks_identity_provider_config.go new file mode 100644 index 00000000000..452fee75d96 --- /dev/null +++ b/aws/resource_aws_eks_identity_provider_config.go @@ -0,0 +1,357 @@ +package aws + +import ( + "context" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfeks "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsEksIdentityProviderConfig() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAwsEksIdentityProviderConfigCreate, + ReadContext: resourceAwsEksIdentityProviderConfigRead, + UpdateContext: resourceAwsEksIdentityProviderConfigUpdate, + DeleteContext: resourceAwsEksIdentityProviderConfigDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + CustomizeDiff: SetTagsDiff, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(25 * time.Minute), + Delete: schema.DefaultTimeout(25 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "cluster_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + + "oidc": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "groups_claim": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "groups_prefix": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "identity_provider_config_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "issuer_url": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.IsURLWithHTTPS, + }, + "required_claims": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + ValidateDiagFunc: allDiagFunc( + validation.MapKeyLenBetween(1, 63), + validation.MapValueLenBetween(1, 253), + ), + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "username_claim": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "username_prefix": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + }, + }, + }, + + "status": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + } +} + +// https://github.com/hashicorp/terraform-plugin-sdk/issues/780. +func allDiagFunc(validators ...schema.SchemaValidateDiagFunc) schema.SchemaValidateDiagFunc { + return func(i interface{}, k cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + for _, validator := range validators { + diags = append(diags, validator(i, k)...) + } + return diags + } +} + +func resourceAwsEksIdentityProviderConfigCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).eksconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + clusterName := d.Get("cluster_name").(string) + configName, oidc := expandEksOidcIdentityProviderConfigRequest(d.Get("oidc").([]interface{})[0].(map[string]interface{})) + id := tfeks.IdentityProviderConfigCreateResourceID(clusterName, configName) + + input := &eks.AssociateIdentityProviderConfigInput{ + ClientRequestToken: aws.String(resource.UniqueId()), + ClusterName: aws.String(clusterName), + Oidc: oidc, + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().EksTags() + } + + _, err := conn.AssociateIdentityProviderConfig(input) + + if err != nil { + return diag.Errorf("error associating EKS Identity Provider Config (%s): %s", id, err) + } + + d.SetId(id) + + _, err = waiter.OidcIdentityProviderConfigCreated(ctx, conn, clusterName, configName, d.Timeout(schema.TimeoutCreate)) + + if err != nil { + return diag.Errorf("error waiting for EKS Identity Provider Config (%s) association: %s", d.Id(), err) + } + + return resourceAwsEksIdentityProviderConfigRead(ctx, d, meta) +} + +func resourceAwsEksIdentityProviderConfigRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).eksconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + clusterName, configName, err := tfeks.IdentityProviderConfigParseResourceID(d.Id()) + + if err != nil { + return diag.FromErr(err) + } + + oidc, err := finder.OidcIdentityProviderConfigByClusterNameAndConfigName(ctx, conn, clusterName, configName) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EKS Identity Provider Config (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error reading EKS Identity Provider Config (%s): %s", d.Id(), err) + } + + d.Set("arn", oidc.IdentityProviderConfigArn) + d.Set("cluster_name", oidc.ClusterName) + + if err := d.Set("oidc", []interface{}{flattenEksOidcIdentityProviderConfig(oidc)}); err != nil { + return diag.Errorf("error setting oidc: %s", err) + } + + d.Set("status", oidc.Status) + + tags := keyvaluetags.EksKeyValueTags(oidc.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("error setting tags: %s", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("error setting tags_all: %s", err) + } + + return nil +} + +func resourceAwsEksIdentityProviderConfigUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).eksconn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := keyvaluetags.EksUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.Errorf("error updating tags: %s", err) + } + } + + return resourceAwsEksIdentityProviderConfigRead(ctx, d, meta) +} + +func resourceAwsEksIdentityProviderConfigDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).eksconn + + clusterName, configName, err := tfeks.IdentityProviderConfigParseResourceID(d.Id()) + + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] Disassociating EKS Identity Provider Config: %s", d.Id()) + _, err = conn.DisassociateIdentityProviderConfigWithContext(ctx, &eks.DisassociateIdentityProviderConfigInput{ + ClusterName: aws.String(clusterName), + IdentityProviderConfig: &eks.IdentityProviderConfig{ + Name: aws.String(configName), + Type: aws.String(tfeks.IdentityProviderConfigTypeOidc), + }, + }) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil + } + + if tfawserr.ErrMessageContains(err, eks.ErrCodeInvalidRequestException, "Identity provider config is not associated with cluster") { + return nil + } + + if err != nil { + return diag.Errorf("error disassociating EKS Identity Provider Config (%s): %s", d.Id(), err) + } + + _, err = waiter.OidcIdentityProviderConfigDeleted(ctx, conn, clusterName, configName, d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return diag.Errorf("error waiting for EKS Identity Provider Config (%s) disassociation: %s", d.Id(), err) + } + + return nil +} + +func expandEksOidcIdentityProviderConfigRequest(tfMap map[string]interface{}) (string, *eks.OidcIdentityProviderConfigRequest) { + if tfMap == nil { + return "", nil + } + + apiObject := &eks.OidcIdentityProviderConfigRequest{} + + if v, ok := tfMap["client_id"].(string); ok && v != "" { + apiObject.ClientId = aws.String(v) + } + + if v, ok := tfMap["groups_claim"].(string); ok && v != "" { + apiObject.GroupsClaim = aws.String(v) + } + + if v, ok := tfMap["groups_prefix"].(string); ok && v != "" { + apiObject.GroupsPrefix = aws.String(v) + } + + var identityProviderConfigName string + if v, ok := tfMap["identity_provider_config_name"].(string); ok && v != "" { + identityProviderConfigName = v + apiObject.IdentityProviderConfigName = aws.String(v) + } + + if v, ok := tfMap["issuer_url"].(string); ok && v != "" { + apiObject.IssuerUrl = aws.String(v) + } + + if v, ok := tfMap["required_claims"].(map[string]interface{}); ok && len(v) > 0 { + apiObject.RequiredClaims = expandStringMap(v) + } + + if v, ok := tfMap["username_claim"].(string); ok && v != "" { + apiObject.UsernameClaim = aws.String(v) + } + + if v, ok := tfMap["username_prefix"].(string); ok && v != "" { + apiObject.UsernamePrefix = aws.String(v) + } + + return identityProviderConfigName, apiObject +} + +func flattenEksOidcIdentityProviderConfig(apiObject *eks.OidcIdentityProviderConfig) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.ClientId; v != nil { + tfMap["client_id"] = aws.StringValue(v) + } + + if v := apiObject.GroupsClaim; v != nil { + tfMap["groups_claim"] = aws.StringValue(v) + } + + if v := apiObject.GroupsPrefix; v != nil { + tfMap["groups_prefix"] = aws.StringValue(v) + } + + if v := apiObject.IdentityProviderConfigName; v != nil { + tfMap["identity_provider_config_name"] = aws.StringValue(v) + } + + if v := apiObject.IssuerUrl; v != nil { + tfMap["issuer_url"] = aws.StringValue(v) + } + + if v := apiObject.RequiredClaims; v != nil { + tfMap["required_claims"] = aws.StringValueMap(v) + } + + if v := apiObject.UsernameClaim; v != nil { + tfMap["username_claim"] = aws.StringValue(v) + } + + if v := apiObject.UsernamePrefix; v != nil { + tfMap["username_prefix"] = aws.StringValue(v) + } + + return tfMap +} diff --git a/aws/resource_aws_eks_identity_provider_config_test.go b/aws/resource_aws_eks_identity_provider_config_test.go new file mode 100644 index 00000000000..db33da83f0f --- /dev/null +++ b/aws/resource_aws_eks_identity_provider_config_test.go @@ -0,0 +1,452 @@ +package aws + +import ( + "context" + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + tfeks "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func init() { + resource.AddTestSweepers("aws_eks_identity_provider_config", &resource.Sweeper{ + Name: "aws_eks_identity_provider_config", + F: testSweepEksIdentityProviderConfigs, + }) +} + +func testSweepEksIdentityProviderConfigs(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + ctx := context.TODO() + conn := client.(*AWSClient).eksconn + input := &eks.ListClustersInput{} + var sweeperErrs *multierror.Error + sweepResources := make([]*testSweepResource, 0) + + err = conn.ListClustersPagesWithContext(ctx, input, func(page *eks.ListClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, cluster := range page.Clusters { + input := &eks.ListIdentityProviderConfigsInput{ + ClusterName: cluster, + } + + err := conn.ListIdentityProviderConfigsPagesWithContext(ctx, input, func(page *eks.ListIdentityProviderConfigsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, identityProviderConfig := range page.IdentityProviderConfigs { + r := resourceAwsEksIdentityProviderConfig() + d := r.Data(nil) + d.SetId(tfeks.IdentityProviderConfigCreateResourceID(aws.StringValue(cluster), aws.StringValue(identityProviderConfig.Name))) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing EKS Identity Provider Configs (%s): %w", region, err)) + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Print(fmt.Errorf("[WARN] Skipping EKS Identity Provider Configs sweep for %s: %w", region, err)) + return sweeperErrs // In case we have completed some pages, but had errors + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing EKS Clusters (%s): %w", region, err)) + } + + err = testSweepResourceOrchestrator(sweepResources) + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error sweeping EKS Identity Provider Configs (%s): %w", region, err)) + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAWSEksIdentityProviderConfig_basic(t *testing.T) { + var config eks.OidcIdentityProviderConfig + rName := acctest.RandomWithPrefix("tf-acc-test") + eksClusterResourceName := "aws_eks_cluster.test" + resourceName := "aws_eks_identity_provider_config.test" + ctx := context.TODO() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAWSEksIdentityProviderConfigDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksIdentityProviderConfigConfigIssuerUrl(rName, "http://example.com"), + ExpectError: regexp.MustCompile(`expected .* to have a url with schema of: "https", got http://example.com`), + }, + { + Config: testAccAWSEksIdentityProviderConfigConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksIdentityProviderConfigExists(ctx, resourceName, &config), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "eks", regexp.MustCompile(fmt.Sprintf("identityproviderconfig/%[1]s/oidc/%[1]s/.+", rName))), + resource.TestCheckResourceAttrPair(resourceName, "cluster_name", eksClusterResourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "oidc.#", "1"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.client_id", "example.net"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.groups_claim", ""), + resource.TestCheckResourceAttr(resourceName, "oidc.0.groups_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "oidc.0.identity_provider_config_name", rName), + resource.TestCheckResourceAttr(resourceName, "oidc.0.issuer_url", "https://example.com"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.required_claims.%", "0"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.username_claim", ""), + resource.TestCheckResourceAttr(resourceName, "oidc.0.username_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSEksIdentityProviderConfig_disappears(t *testing.T) { + var config eks.OidcIdentityProviderConfig + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_eks_identity_provider_config.test" + ctx := context.TODO() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAWSEksIdentityProviderConfigDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksIdentityProviderConfigConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksIdentityProviderConfigExists(ctx, resourceName, &config), + testAccCheckResourceDisappears(testAccProvider, resourceAwsEksIdentityProviderConfig(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSEksIdentityProviderConfig_AllOidcOptions(t *testing.T) { + var config eks.OidcIdentityProviderConfig + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_eks_identity_provider_config.test" + ctx := context.TODO() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAWSEksIdentityProviderConfigDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksIdentityProviderConfigAllOidcOptions(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksIdentityProviderConfigExists(ctx, resourceName, &config), + resource.TestCheckResourceAttr(resourceName, "oidc.#", "1"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.client_id", "example.net"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.groups_claim", "groups"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.groups_prefix", "oidc:"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.identity_provider_config_name", rName), + resource.TestCheckResourceAttr(resourceName, "oidc.0.issuer_url", "https://example.com"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.required_claims.%", "2"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.required_claims.keyOne", "valueOne"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.required_claims.keyTwo", "valueTwo"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.username_claim", "email"), + resource.TestCheckResourceAttr(resourceName, "oidc.0.username_prefix", "-"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSEksIdentityProviderConfig_Tags(t *testing.T) { + var config eks.OidcIdentityProviderConfig + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_eks_identity_provider_config.test" + ctx := context.TODO() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAWSEksIdentityProviderConfigDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksIdentityProviderConfigConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksIdentityProviderConfigExists(ctx, resourceName, &config), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSEksIdentityProviderConfigConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksIdentityProviderConfigExists(ctx, resourceName, &config), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSEksIdentityProviderConfigConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksIdentityProviderConfigExists(ctx, resourceName, &config), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAWSEksIdentityProviderConfigExists(ctx context.Context, resourceName string, config *eks.OidcIdentityProviderConfig) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No EKS Identity Profile Config ID is set") + } + + clusterName, configName, err := tfeks.IdentityProviderConfigParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).eksconn + + output, err := finder.OidcIdentityProviderConfigByClusterNameAndConfigName(ctx, conn, clusterName, configName) + + if err != nil { + return err + } + + *config = *output + + return nil + } +} + +func testAccCheckAWSEksIdentityProviderConfigDestroy(s *terraform.State) error { + ctx := context.TODO() + conn := testAccProvider.Meta().(*AWSClient).eksconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_eks_identity_provider_config" { + continue + } + + clusterName, configName, err := tfeks.IdentityProviderConfigParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = finder.OidcIdentityProviderConfigByClusterNameAndConfigName(ctx, conn, clusterName, configName) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("EKS Identity Profile Config %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAWSEksIdentityProviderConfigConfigBase(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = jsonencode({ + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "eks.${data.aws_partition.current.dns_suffix}" + } + }] + Version = "2012-10-17" + }) +} + +resource "aws_iam_role_policy_attachment" "cluster-AmazonEKSClusterPolicy" { + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSClusterPolicy" + role = aws_iam_role.test.name +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = %[1]q + "kubernetes.io/cluster/%[1]s" = "shared" + } +} + +resource "aws_subnet" "test" { + count = 2 + + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = "10.0.${count.index}.0/24" + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + "kubernetes.io/cluster/%[1]s" = "shared" + } +} + +resource "aws_eks_cluster" "test" { + name = %[1]q + role_arn = aws_iam_role.test.arn + + vpc_config { + subnet_ids = aws_subnet.test[*].id + } + + depends_on = [aws_iam_role_policy_attachment.cluster-AmazonEKSClusterPolicy] +} +`, rName)) +} + +func testAccAWSEksIdentityProviderConfigConfigName(rName string) string { + return composeConfig(testAccAWSEksIdentityProviderConfigConfigBase(rName), fmt.Sprintf(` +resource "aws_eks_identity_provider_config" "test" { + cluster_name = aws_eks_cluster.test.name + + oidc { + client_id = "example.net" + identity_provider_config_name = %[1]q + issuer_url = "https://example.com" + } +} +`, rName)) +} + +func testAccAWSEksIdentityProviderConfigConfigIssuerUrl(rName, issuerUrl string) string { + return composeConfig(testAccAWSEksIdentityProviderConfigConfigBase(rName), fmt.Sprintf(` +resource "aws_eks_identity_provider_config" "test" { + cluster_name = aws_eks_cluster.test.name + + oidc { + client_id = "example.net" + identity_provider_config_name = %[1]q + issuer_url = %[2]q + } +} +`, rName, issuerUrl)) +} + +func testAccAWSEksIdentityProviderConfigAllOidcOptions(rName string) string { + return composeConfig(testAccAWSEksIdentityProviderConfigConfigBase(rName), fmt.Sprintf(` +resource "aws_eks_identity_provider_config" "test" { + cluster_name = aws_eks_cluster.test.name + + oidc { + client_id = "example.net" + groups_claim = "groups" + groups_prefix = "oidc:" + identity_provider_config_name = %[1]q + issuer_url = "https://example.com" + username_claim = "email" + username_prefix = "-" + + required_claims = { + keyOne = "valueOne" + keyTwo = "valueTwo" + } + } +} +`, rName)) +} + +func testAccAWSEksIdentityProviderConfigConfigTags1(rName, tagKey1, tagValue1 string) string { + return composeConfig(testAccAWSEksIdentityProviderConfigConfigBase(rName), fmt.Sprintf(` +resource "aws_eks_identity_provider_config" "test" { + cluster_name = aws_eks_cluster.test.name + + oidc { + client_id = "example.net" + identity_provider_config_name = %[1]q + issuer_url = "https://example.com" + } + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1)) +} + +func testAccAWSEksIdentityProviderConfigConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return composeConfig(testAccAWSEksIdentityProviderConfigConfigBase(rName), fmt.Sprintf(` +resource "aws_eks_identity_provider_config" "test" { + cluster_name = aws_eks_cluster.test.name + + oidc { + client_id = "example.net" + identity_provider_config_name = %[1]q + issuer_url = "https://example.com" + } + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} diff --git a/website/docs/r/eks_identity_provider_config.html.markdown b/website/docs/r/eks_identity_provider_config.html.markdown new file mode 100644 index 00000000000..ffe45bc08f5 --- /dev/null +++ b/website/docs/r/eks_identity_provider_config.html.markdown @@ -0,0 +1,68 @@ +--- +subcategory: "EKS" +layout: "aws" +page_title: "AWS: aws_eks_identity_provider_config" +description: |- + Manages an EKS Identity Provider Configuration. +--- + +# Resource: aws_eks_identity_provider_config + +Manages an EKS Identity Provider Configuration. + +## Example Usage + +```terraform +resource "aws_eks_identity_provider_config" "example" { + cluster_name = aws_eks_cluster.example.name + + oidc { + client_id = "your client_id" + identity_provider_config_name = "example" + issuer_url = "your issuer_url" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cluster_name` – (Required) Name of the EKS Cluster. +* `oidc` - (Required) Nested attribute containing [OpenID Connect](https://openid.net/connect/) identity provider information for the cluster. Detailed below. +* `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +### oidc Configuration Block + +* `client_id` – (Required) Client ID for the OpenID Connect identity provider. +* `groups_claim` - (Optional) The JWT claim that the provider will use to return groups. +* `groups_prefix` - (Optional) A prefix that is prepended to group claims e.g. `oidc:`. +* `identity_provider_config_name` – (Required) The name of the identity provider config. +* `issuer_url` - (Required) Issuer URL for the OpenID Connect identity provider. +* `required_claims` - (Optional) The key value pairs that describe required claims in the identity token. +* `username_claim` - (Optional) The JWT claim that the provider will use as the username. +* `username_prefix` - (Optional) A prefix that is prepended to username claims. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of the EKS Identity Provider Configuration. +* `id` - EKS Cluster name and EKS Identity Provider Configuration name separated by a colon (`:`). +* `status` - Status of the EKS Identity Provider Configuration. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block). + +## Timeouts + +`aws_eks_identity_provider_config` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +* `create` - (Default `25 minutes`) How long to wait for the EKS Identity Provider Configuration to be associated. +* `delete` - (Default `25 minutes`) How long to wait for the EKS Identity Provider Configuration to be disassociated. + +## Import + +EKS Identity Provider Configurations can be imported using the `cluster_name` and `identity_provider_config_name` separated by a colon (`:`), e.g. + +``` +$ terraform import aws_eks_identity_provider_config.my_identity_provider_config my_cluster:my_identity_provider_config +```