diff --git a/.changelog/17964.txt b/.changelog/17964.txt new file mode 100644 index 00000000000..f762457c64a --- /dev/null +++ b/.changelog/17964.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_iam_openid_connect_provider: Add tagging support +``` + +```release-note:enhancement +resource/aws_iam_openid_connect_provider: Add plan time validation for `client_id_list` and `thumbprint_list` +``` diff --git a/aws/internal/keyvaluetags/iam_tags.go b/aws/internal/keyvaluetags/iam_tags.go index 3da7f9366a7..279377398f8 100644 --- a/aws/internal/keyvaluetags/iam_tags.go +++ b/aws/internal/keyvaluetags/iam_tags.go @@ -81,6 +81,41 @@ func IamUserUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, return nil } +// IamOpenIDConnectProviderUpdateTags updates IAM OpenID Connect Provider tags. +// The identifier is the OpenID Connect Provider ARN. +func IamOpenIDConnectProviderUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &iam.UntagOpenIDConnectProviderInput{ + OpenIDConnectProviderArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.Keys()), + } + + _, err := conn.UntagOpenIDConnectProvider(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &iam.TagOpenIDConnectProviderInput{ + OpenIDConnectProviderArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().IamTags(), + } + + _, err := conn.TagOpenIDConnectProvider(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // IamSAMLProviderUpdateTags updates IAM SAML Provider tags. // The identifier is the SAML Provider ARN. func IamSAMLProviderUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { diff --git a/aws/resource_aws_iam_openid_connect_provider.go b/aws/resource_aws_iam_openid_connect_provider.go index ed97ba7176e..dadae698b74 100644 --- a/aws/resource_aws_iam_openid_connect_provider.go +++ b/aws/resource_aws_iam_openid_connect_provider.go @@ -7,6 +7,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" "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" ) func resourceAwsIamOpenIDConnectProvider() *schema.Resource { @@ -32,30 +34,38 @@ func resourceAwsIamOpenIDConnectProvider() *schema.Resource { DiffSuppressFunc: suppressOpenIdURL, }, "client_id_list": { - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(1, 255), + }, Type: schema.TypeList, Required: true, ForceNew: true, }, "thumbprint_list": { - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(40, 40), + }, Type: schema.TypeList, Required: true, }, + "tags": tagsSchema(), }, } } func resourceAwsIamOpenIDConnectProviderCreate(d *schema.ResourceData, meta interface{}) error { - iamconn := meta.(*AWSClient).iamconn + conn := meta.(*AWSClient).iamconn input := &iam.CreateOpenIDConnectProviderInput{ Url: aws.String(d.Get("url").(string)), ClientIDList: expandStringList(d.Get("client_id_list").([]interface{})), ThumbprintList: expandStringList(d.Get("thumbprint_list").([]interface{})), + Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().IamTags(), } - out, err := iamconn.CreateOpenIDConnectProvider(input) + out, err := conn.CreateOpenIDConnectProvider(input) if err != nil { return fmt.Errorf("error creating IAM OIDC Provider: %w", err) } @@ -66,12 +76,13 @@ func resourceAwsIamOpenIDConnectProviderCreate(d *schema.ResourceData, meta inte } func resourceAwsIamOpenIDConnectProviderRead(d *schema.ResourceData, meta interface{}) error { - iamconn := meta.(*AWSClient).iamconn + conn := meta.(*AWSClient).iamconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := &iam.GetOpenIDConnectProviderInput{ OpenIDConnectProviderArn: aws.String(d.Id()), } - out, err := iamconn.GetOpenIDConnectProvider(input) + out, err := conn.GetOpenIDConnectProvider(input) if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { log.Printf("[WARN] IAM OIDC Provider (%s) not found, removing from state", d.Id()) d.SetId("") @@ -86,11 +97,15 @@ func resourceAwsIamOpenIDConnectProviderRead(d *schema.ResourceData, meta interf d.Set("client_id_list", flattenStringList(out.ClientIDList)) d.Set("thumbprint_list", flattenStringList(out.ThumbprintList)) + if err := d.Set("tags", keyvaluetags.IamKeyValueTags(out.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + return nil } func resourceAwsIamOpenIDConnectProviderUpdate(d *schema.ResourceData, meta interface{}) error { - iamconn := meta.(*AWSClient).iamconn + conn := meta.(*AWSClient).iamconn if d.HasChange("thumbprint_list") { input := &iam.UpdateOpenIDConnectProviderThumbprintInput{ @@ -98,22 +113,30 @@ func resourceAwsIamOpenIDConnectProviderUpdate(d *schema.ResourceData, meta inte ThumbprintList: expandStringList(d.Get("thumbprint_list").([]interface{})), } - _, err := iamconn.UpdateOpenIDConnectProviderThumbprint(input) + _, err := conn.UpdateOpenIDConnectProviderThumbprint(input) if err != nil { return fmt.Errorf("error updating IAM OIDC Provider (%s) thumbprint: %w", d.Id(), err) } } + if d.HasChange("tags") { + o, n := d.GetChange("tags") + + if err := keyvaluetags.IamOpenIDConnectProviderUpdateTags(conn, d.Id(), o, n); err != nil { + return fmt.Errorf("error updating tags for IAM OIDC Provider (%s): %w", d.Id(), err) + } + } + return resourceAwsIamOpenIDConnectProviderRead(d, meta) } func resourceAwsIamOpenIDConnectProviderDelete(d *schema.ResourceData, meta interface{}) error { - iamconn := meta.(*AWSClient).iamconn + conn := meta.(*AWSClient).iamconn input := &iam.DeleteOpenIDConnectProviderInput{ OpenIDConnectProviderArn: aws.String(d.Id()), } - _, err := iamconn.DeleteOpenIDConnectProvider(input) + _, err := conn.DeleteOpenIDConnectProvider(input) if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { return nil } diff --git a/aws/resource_aws_iam_openid_connect_provider_test.go b/aws/resource_aws_iam_openid_connect_provider_test.go index c34cdba9bc6..6a476a3b3d4 100644 --- a/aws/resource_aws_iam_openid_connect_provider_test.go +++ b/aws/resource_aws_iam_openid_connect_provider_test.go @@ -2,16 +2,63 @@ package aws import ( "fmt" + "log" "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/iam" + "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" ) +func init() { + resource.AddTestSweepers("aws_iam_openid_connect_provider", &resource.Sweeper{ + Name: "aws_iam_openid_connect_provider", + F: testSweepIamOpenIDConnectProvider, + }) +} + +func testSweepIamOpenIDConnectProvider(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*AWSClient).iamconn + + var sweeperErrs *multierror.Error + + out, err := conn.ListOpenIDConnectProviders(&iam.ListOpenIDConnectProvidersInput{}) + + for _, oidcProvider := range out.OpenIDConnectProviderList { + arn := aws.StringValue(oidcProvider.Arn) + + r := resourceAwsIamOpenIDConnectProvider() + d := r.Data(nil) + d.SetId(arn) + err := r.Delete(d, client) + + if err != nil { + sweeperErr := fmt.Errorf("error deleting IAM OIDC Provider (%s): %w", arn, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + continue + } + } + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping IAM OIDC Provider sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error describing IAM OIDC Providers: %w", err)) + } + + return sweeperErrs.ErrorOrNil() +} + func TestAccAWSIAMOpenIDConnectProvider_basic(t *testing.T) { rString := acctest.RandString(5) url := "accounts.testle.com/" + rString @@ -27,11 +74,13 @@ func TestAccAWSIAMOpenIDConnectProvider_basic(t *testing.T) { Config: testAccIAMOpenIDConnectProviderConfig(rString), Check: resource.ComposeTestCheckFunc( testAccCheckIAMOpenIDConnectProvider(resourceName), + testAccCheckResourceAttrGlobalARN(resourceName, "arn", "iam", fmt.Sprintf("oidc-provider/%s", url)), resource.TestCheckResourceAttr(resourceName, "url", url), resource.TestCheckResourceAttr(resourceName, "client_id_list.#", "1"), resource.TestCheckResourceAttr(resourceName, "client_id_list.0", "266362248691-re108qaeld573ia0l6clj2i5ac7r7291.apps.testleusercontent.com"), resource.TestCheckResourceAttr(resourceName, "thumbprint_list.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, { @@ -50,6 +99,52 @@ func TestAccAWSIAMOpenIDConnectProvider_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "thumbprint_list.#", "2"), resource.TestCheckResourceAttr(resourceName, "thumbprint_list.0", "cf23df2207d99a74fbe169e3eba035e633b65d94"), resource.TestCheckResourceAttr(resourceName, "thumbprint_list.1", "c784713d6f9cb67b55dd84f4e4af7832d42b8f55"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccAWSIAMOpenIDConnectProvider_tags(t *testing.T) { + rString := acctest.RandString(5) + resourceName := "aws_iam_openid_connect_provider.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSInstanceProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccIAMOpenIDConnectProviderConfigTags1(rString, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMOpenIDConnectProvider(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name_prefix"}, + }, + { + Config: testAccIAMOpenIDConnectProviderConfigTags2(rString, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMOpenIDConnectProvider(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccIAMOpenIDConnectProviderConfigTags1(rString, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMOpenIDConnectProvider(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), ), }, }, @@ -70,7 +165,7 @@ func TestAccAWSIAMOpenIDConnectProvider_disappears(t *testing.T) { Config: testAccIAMOpenIDConnectProviderConfig(rString), Check: resource.ComposeTestCheckFunc( testAccCheckIAMOpenIDConnectProvider(resourceName), - testAccCheckIAMOpenIDConnectProviderDisappears(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsIamOpenIDConnectProvider(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -79,7 +174,7 @@ func TestAccAWSIAMOpenIDConnectProvider_disappears(t *testing.T) { } func testAccCheckIAMOpenIDConnectProviderDestroy(s *terraform.State) error { - iamconn := testAccProvider.Meta().(*AWSClient).iamconn + conn := testAccProvider.Meta().(*AWSClient).iamconn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_iam_openid_connect_provider" { @@ -89,13 +184,13 @@ func testAccCheckIAMOpenIDConnectProviderDestroy(s *terraform.State) error { input := &iam.GetOpenIDConnectProviderInput{ OpenIDConnectProviderArn: aws.String(rs.Primary.ID), } - out, err := iamconn.GetOpenIDConnectProvider(input) + out, err := conn.GetOpenIDConnectProvider(input) if err != nil { - if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" { + if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { // none found, that's good return nil } - return fmt.Errorf("Error reading IAM OpenID Connect Provider, out: %s, err: %s", out, err) + return fmt.Errorf("Error reading IAM OpenID Connect Provider, out: %s, err: %w", out, err) } if out != nil { @@ -106,25 +201,6 @@ func testAccCheckIAMOpenIDConnectProviderDestroy(s *terraform.State) error { return nil } -func testAccCheckIAMOpenIDConnectProviderDisappears(id string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not Found: %s", id) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - iamconn := testAccProvider.Meta().(*AWSClient).iamconn - _, err := iamconn.DeleteOpenIDConnectProvider(&iam.DeleteOpenIDConnectProviderInput{ - OpenIDConnectProviderArn: aws.String(rs.Primary.ID), - }) - return err - } -} - func testAccCheckIAMOpenIDConnectProvider(id string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[id] @@ -136,8 +212,8 @@ func testAccCheckIAMOpenIDConnectProvider(id string) resource.TestCheckFunc { return fmt.Errorf("No ID is set") } - iamconn := testAccProvider.Meta().(*AWSClient).iamconn - _, err := iamconn.GetOpenIDConnectProvider(&iam.GetOpenIDConnectProviderInput{ + conn := testAccProvider.Meta().(*AWSClient).iamconn + _, err := conn.GetOpenIDConnectProvider(&iam.GetOpenIDConnectProviderInput{ OpenIDConnectProviderArn: aws.String(rs.Primary.ID), }) @@ -172,3 +248,40 @@ resource "aws_iam_openid_connect_provider" "test" { } `, rString) } + +func testAccIAMOpenIDConnectProviderConfigTags1(rString, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_iam_openid_connect_provider" "test" { + url = "https://accounts.testle.com/%s" + + client_id_list = [ + "266362248691-re108qaeld573ia0l6clj2i5ac7r7291.apps.testleusercontent.com", + ] + + thumbprint_list = [] + + tags = { + %[2]q = %[3]q + } +} +`, rString, tagKey1, tagValue1) +} + +func testAccIAMOpenIDConnectProviderConfigTags2(rString, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_iam_openid_connect_provider" "test" { + url = "https://accounts.testle.com/%s" + + client_id_list = [ + "266362248691-re108qaeld573ia0l6clj2i5ac7r7291.apps.testleusercontent.com", + ] + + thumbprint_list = [] + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rString, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/website/docs/r/iam_openid_connect_provider.html.markdown b/website/docs/r/iam_openid_connect_provider.html.markdown index 7ce78dd022f..720dd94a793 100644 --- a/website/docs/r/iam_openid_connect_provider.html.markdown +++ b/website/docs/r/iam_openid_connect_provider.html.markdown @@ -31,6 +31,7 @@ The following arguments are supported: * `url` - (Required) The URL of the identity provider. Corresponds to the _iss_ claim. * `client_id_list` - (Required) A list of client IDs (also known as audiences). When a mobile or web app registers with an OpenID Connect provider, they establish a value that identifies the application. (This is the value that's sent as the client_id parameter on OAuth requests.) * `thumbprint_list` - (Required) A list of server certificate thumbprints for the OpenID Connect (OIDC) identity provider's server certificate(s). +* `tags` - (Optional) Map of resource tags for the IAM OIDC provider. ## Attributes Reference