diff --git a/aws/internal/service/glue/finder/finder.go b/aws/internal/service/glue/finder/finder.go new file mode 100644 index 000000000000..5218f3c531cd --- /dev/null +++ b/aws/internal/service/glue/finder/finder.go @@ -0,0 +1,20 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/service/glue" + tfglue "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue" +) + +// RegistryByID returns the Registry corresponding to the specified ID. +func RegistryByID(conn *glue.Glue, id string) (*glue.GetRegistryOutput, error) { + input := &glue.GetRegistryInput{ + RegistryId: tfglue.CreateAwsGlueRegistryID(id), + } + + output, err := conn.GetRegistry(input) + if err != nil { + return nil, err + } + + return output, nil +} diff --git a/aws/internal/service/glue/id.go b/aws/internal/service/glue/id.go index ec3f3f8b43d2..3eeada183f33 100644 --- a/aws/internal/service/glue/id.go +++ b/aws/internal/service/glue/id.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/glue" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -30,3 +32,9 @@ func stringifyAwsGluePartition(partValues *schema.Set) string { return vals } + +func CreateAwsGlueRegistryID(id string) *glue.RegistryId { + return &glue.RegistryId{ + RegistryArn: aws.String(id), + } +} diff --git a/aws/internal/service/glue/waiter/status.go b/aws/internal/service/glue/waiter/status.go index 8afe0a119261..4f653d3fd206 100644 --- a/aws/internal/service/glue/waiter/status.go +++ b/aws/internal/service/glue/waiter/status.go @@ -7,10 +7,12 @@ import ( "github.com/aws/aws-sdk-go/service/glue" "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue/finder" ) const ( MLTransformStatusUnknown = "Unknown" + RegistryStatusUnknown = "Unknown" TriggerStatusUnknown = "Unknown" ) @@ -35,6 +37,22 @@ func MLTransformStatus(conn *glue.Glue, transformId string) resource.StateRefres } } +// RegistryStatus fetches the Registry and its Status +func RegistryStatus(conn *glue.Glue, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.RegistryByID(conn, id) + if err != nil { + return nil, RegistryStatusUnknown, err + } + + if output == nil { + return output, RegistryStatusUnknown, nil + } + + return output, aws.StringValue(output.Status), nil + } +} + // TriggerStatus fetches the Trigger and its Status func TriggerStatus(conn *glue.Glue, triggerName string) resource.StateRefreshFunc { return func() (interface{}, string, error) { diff --git a/aws/internal/service/glue/waiter/waiter.go b/aws/internal/service/glue/waiter/waiter.go index 3ca45d967cb9..36772e72261e 100644 --- a/aws/internal/service/glue/waiter/waiter.go +++ b/aws/internal/service/glue/waiter/waiter.go @@ -10,6 +10,7 @@ import ( const ( // Maximum amount of time to wait for an Operation to return Deleted MLTransformDeleteTimeout = 2 * time.Minute + RegistryDeleteTimeout = 2 * time.Minute TriggerCreateTimeout = 2 * time.Minute TriggerDeleteTimeout = 2 * time.Minute ) @@ -32,6 +33,24 @@ func MLTransformDeleted(conn *glue.Glue, transformId string) (*glue.GetMLTransfo return nil, err } +// RegistryDeleted waits for a Registry to return Deleted +func RegistryDeleted(conn *glue.Glue, registryID string) (*glue.GetRegistryOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{glue.RegistryStatusDeleting}, + Target: []string{}, + Refresh: RegistryStatus(conn, registryID), + Timeout: RegistryDeleteTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*glue.GetRegistryOutput); ok { + return output, err + } + + return nil, err +} + // TriggerCreated waits for a Trigger to return Created func TriggerCreated(conn *glue.Glue, triggerName string) (*glue.GetTriggerOutput, error) { stateConf := &resource.StateChangeConf{ diff --git a/aws/provider.go b/aws/provider.go index 1b8bd8c38961..b35ac724b0d8 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -671,6 +671,7 @@ func Provider() *schema.Provider { "aws_glue_job": resourceAwsGlueJob(), "aws_glue_ml_transform": resourceAwsGlueMLTransform(), "aws_glue_partition": resourceAwsGluePartition(), + "aws_glue_registry": resourceAwsGlueRegistry(), "aws_glue_security_configuration": resourceAwsGlueSecurityConfiguration(), "aws_glue_trigger": resourceAwsGlueTrigger(), "aws_glue_user_defined_function": resourceAwsGlueUserDefinedFunction(), diff --git a/aws/resource_aws_glue_registry.go b/aws/resource_aws_glue_registry.go new file mode 100644 index 000000000000..01912753b35d --- /dev/null +++ b/aws/resource_aws_glue_registry.go @@ -0,0 +1,166 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/glue" + "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" + tfglue "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue/waiter" +) + +func resourceAwsGlueRegistry() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsGlueRegistryCreate, + Read: resourceAwsGlueRegistryRead, + Update: resourceAwsGlueRegistryUpdate, + Delete: resourceAwsGlueRegistryDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 2048), + }, + "registry_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 255), + validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9-_$#]+$`), ""), + ), + }, + "tags": tagsSchema(), + }, + } +} + +func resourceAwsGlueRegistryCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + + input := &glue.CreateRegistryInput{ + RegistryName: aws.String(d.Get("registry_name").(string)), + Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().GlueTags(), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating Glue Registry: %s", input) + output, err := conn.CreateRegistry(input) + if err != nil { + return fmt.Errorf("error creating Glue Registry: %w", err) + } + d.SetId(aws.StringValue(output.RegistryArn)) + + return resourceAwsGlueRegistryRead(d, meta) +} + +func resourceAwsGlueRegistryRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + output, err := finder.RegistryByID(conn, d.Id()) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + log.Printf("[WARN] Glue Registry (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("error reading Glue Registry (%s): %w", d.Id(), err) + } + + if output == nil { + log.Printf("[WARN] Glue Registry (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + arn := aws.StringValue(output.RegistryArn) + d.Set("arn", arn) + d.Set("description", output.Description) + d.Set("registry_name", output.RegistryName) + + tags, err := keyvaluetags.GlueListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for Glue Registry (%s): %w", arn, err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return nil +} + +func resourceAwsGlueRegistryUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + + if d.HasChanges("description") { + input := &glue.UpdateRegistryInput{ + RegistryId: tfglue.CreateAwsGlueRegistryID(d.Id()), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Updating Glue Registry: %#v", input) + _, err := conn.UpdateRegistry(input) + if err != nil { + return fmt.Errorf("error updating Glue Registry (%s): %w", d.Id(), err) + } + } + + if d.HasChange("tags") { + o, n := d.GetChange("tags") + if err := keyvaluetags.GlueUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating tags: %s", err) + } + } + + return resourceAwsGlueRegistryRead(d, meta) +} + +func resourceAwsGlueRegistryDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + + log.Printf("[DEBUG] Deleting Glue Registry: %s", d.Id()) + input := &glue.DeleteRegistryInput{ + RegistryId: tfglue.CreateAwsGlueRegistryID(d.Id()), + } + + _, err := conn.DeleteRegistry(input) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + return nil + } + return fmt.Errorf("error deleting Glue Registry (%s): %w", d.Id(), err) + } + + _, err = waiter.RegistryDeleted(conn, d.Id()) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + return nil + } + return fmt.Errorf("error waiting for Glue Registry (%s) to be deleted: %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_glue_registry_test.go b/aws/resource_aws_glue_registry_test.go new file mode 100644 index 000000000000..56b044967b50 --- /dev/null +++ b/aws/resource_aws_glue_registry_test.go @@ -0,0 +1,294 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/glue" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue/finder" +) + +func init() { + resource.AddTestSweepers("aws_glue_registry", &resource.Sweeper{ + Name: "aws_glue_registry", + F: testSweepGlueRegistry, + }) +} + +func testSweepGlueRegistry(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).glueconn + + listOutput, err := conn.ListRegistries(&glue.ListRegistriesInput{}) + if err != nil { + // Some endpoints that do not support Glue Registrys return InternalFailure + if testSweepSkipSweepError(err) || isAWSErr(err, "InternalFailure", "") { + log.Printf("[WARN] Skipping Glue Registry sweep for %s: %s", region, err) + return nil + } + return fmt.Errorf("Error retrieving Glue Registry: %s", err) + } + for _, registry := range listOutput.Registries { + arn := aws.StringValue(registry.RegistryArn) + r := resourceAwsGlueRegistry() + d := r.Data(nil) + d.SetId(arn) + + err := r.Delete(d, client) + if err != nil { + log.Printf("[ERROR] Failed to delete Glue Registry %s: %s", arn, err) + } + } + return nil +} + +func TestAccAWSGlueRegistry_basic(t *testing.T) { + var registry glue.GetRegistryOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_registry.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueRegistry(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueRegistryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueRegistryBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueRegistryExists(resourceName, ®istry), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "glue", fmt.Sprintf("registry/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "registry_name", rName), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSGlueRegistry_Description(t *testing.T) { + var registry glue.GetRegistryOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_registry.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueRegistry(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueRegistryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueRegistryDescriptionConfig(rName, "First Description"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueRegistryExists(resourceName, ®istry), + resource.TestCheckResourceAttr(resourceName, "description", "First Description"), + ), + }, + { + Config: testAccAWSGlueRegistryDescriptionConfig(rName, "Second Description"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueRegistryExists(resourceName, ®istry), + resource.TestCheckResourceAttr(resourceName, "description", "Second Description"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSGlueRegistry_tags(t *testing.T) { + var registry glue.GetRegistryOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_registry.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueRegistry(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueRegistryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueRegistryConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueRegistryExists(resourceName, ®istry), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSGlueRegistryConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueRegistryExists(resourceName, ®istry), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSGlueRegistryConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueRegistryExists(resourceName, ®istry), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccAWSGlueRegistry_disappears(t *testing.T) { + var registry glue.GetRegistryOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_registry.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueRegistry(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueRegistryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueRegistryBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueRegistryExists(resourceName, ®istry), + testAccCheckResourceDisappears(testAccProvider, resourceAwsGlueRegistry(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccPreCheckAWSGlueRegistry(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).glueconn + + _, err := conn.ListRegistries(&glue.ListRegistriesInput{}) + + // Some endpoints that do not support Glue Registrys return InternalFailure + if testAccPreCheckSkipError(err) || isAWSErr(err, "InternalFailure", "") { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCheckAWSGlueRegistryExists(resourceName string, registry *glue.GetRegistryOutput) 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 Glue Registry ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).glueconn + output, err := finder.RegistryByID(conn, rs.Primary.ID) + if err != nil { + return err + } + + if output == nil { + return fmt.Errorf("Glue Registry (%s) not found", rs.Primary.ID) + } + + if aws.StringValue(output.RegistryArn) == rs.Primary.ID { + *registry = *output + return nil + } + + return fmt.Errorf("Glue Registry (%s) not found", rs.Primary.ID) + } +} + +func testAccCheckAWSGlueRegistryDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_glue_registry" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).glueconn + output, err := finder.RegistryByID(conn, rs.Primary.ID) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + return nil + } + + } + + if output != nil && aws.StringValue(output.RegistryArn) == rs.Primary.ID { + return fmt.Errorf("Glue Registry %s still exists", rs.Primary.ID) + } + + return err + } + + return nil +} + +func testAccAWSGlueRegistryDescriptionConfig(rName, description string) string { + return fmt.Sprintf(` +resource "aws_glue_registry" "test" { + registry_name = %[1]q + description = %[2]q +} +`, rName, description) +} + +func testAccAWSGlueRegistryBasicConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_glue_registry" "test" { + registry_name = %[1]q +} +`, rName) +} + +func testAccAWSGlueRegistryConfigTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_glue_registry" "test" { + registry_name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSGlueRegistryConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_glue_registry" "test" { + registry_name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/website/docs/r/glue_registry.html.markdown b/website/docs/r/glue_registry.html.markdown new file mode 100644 index 000000000000..37db026e62b6 --- /dev/null +++ b/website/docs/r/glue_registry.html.markdown @@ -0,0 +1,42 @@ +--- +subcategory: "Glue" +layout: "aws" +page_title: "AWS: aws_glue_registry" +description: |- + Provides a Glue Registry resource. +--- + +# Resource: aws_glue_registry + +Provides a Glue Registry resource. + +## Example Usage + +```hcl +resource "aws_glue_registry" "example" { + registry_name = "example" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `registry_name` – (Required) The Name of the registry. +* `description` – (Optional) A description of the registry. +* `tags` - (Optional) Key-value map of resource tags + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of Glue Registry. +* `id` - Amazon Resource Name (ARN) of Glue Registry. + +## Import + +Glue Registries can be imported using `arn`, e.g. + +``` +$ terraform import aws_glue_registry.example arn:aws:glue:us-west-2:123456789012:registry/example +```