diff --git a/aws/internal/service/glue/finder/finder.go b/aws/internal/service/glue/finder/finder.go index 5218f3c531c..fdceee9166b 100644 --- a/aws/internal/service/glue/finder/finder.go +++ b/aws/internal/service/glue/finder/finder.go @@ -1,6 +1,7 @@ package finder import ( + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/glue" tfglue "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue" ) @@ -18,3 +19,34 @@ func RegistryByID(conn *glue.Glue, id string) (*glue.GetRegistryOutput, error) { return output, nil } + +// SchemaByID returns the Schema corresponding to the specified ID. +func SchemaByID(conn *glue.Glue, id string) (*glue.GetSchemaOutput, error) { + input := &glue.GetSchemaInput{ + SchemaId: tfglue.CreateAwsGlueSchemaID(id), + } + + output, err := conn.GetSchema(input) + if err != nil { + return nil, err + } + + return output, nil +} + +// SchemaVersionByID returns the Schema corresponding to the specified ID. +func SchemaVersionByID(conn *glue.Glue, id string) (*glue.GetSchemaVersionOutput, error) { + input := &glue.GetSchemaVersionInput{ + SchemaId: tfglue.CreateAwsGlueSchemaID(id), + SchemaVersionNumber: &glue.SchemaVersionNumber{ + LatestVersion: aws.Bool(true), + }, + } + + output, err := conn.GetSchemaVersion(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 3eeada183f3..2bc05fff411 100644 --- a/aws/internal/service/glue/id.go +++ b/aws/internal/service/glue/id.go @@ -38,3 +38,9 @@ func CreateAwsGlueRegistryID(id string) *glue.RegistryId { RegistryArn: aws.String(id), } } + +func CreateAwsGlueSchemaID(id string) *glue.SchemaId { + return &glue.SchemaId{ + SchemaArn: aws.String(id), + } +} diff --git a/aws/internal/service/glue/waiter/status.go b/aws/internal/service/glue/waiter/status.go index 4f653d3fd20..ed80b5ed589 100644 --- a/aws/internal/service/glue/waiter/status.go +++ b/aws/internal/service/glue/waiter/status.go @@ -11,9 +11,11 @@ import ( ) const ( - MLTransformStatusUnknown = "Unknown" - RegistryStatusUnknown = "Unknown" - TriggerStatusUnknown = "Unknown" + MLTransformStatusUnknown = "Unknown" + RegistryStatusUnknown = "Unknown" + SchemaStatusUnknown = "Unknown" + SchemaVersionStatusUnknown = "Unknown" + TriggerStatusUnknown = "Unknown" ) // MLTransformStatus fetches the MLTransform and its Status @@ -53,6 +55,38 @@ func RegistryStatus(conn *glue.Glue, id string) resource.StateRefreshFunc { } } +// SchemaStatus fetches the Schema and its Status +func SchemaStatus(conn *glue.Glue, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.SchemaByID(conn, id) + if err != nil { + return nil, SchemaStatusUnknown, err + } + + if output == nil { + return output, SchemaStatusUnknown, nil + } + + return output, aws.StringValue(output.SchemaStatus), nil + } +} + +// SchemaVersionStatus fetches the Schema Version and its Status +func SchemaVersionStatus(conn *glue.Glue, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.SchemaVersionByID(conn, id) + if err != nil { + return nil, SchemaVersionStatusUnknown, err + } + + if output == nil { + return output, SchemaVersionStatusUnknown, 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 36772e72261..aea848fa40e 100644 --- a/aws/internal/service/glue/waiter/waiter.go +++ b/aws/internal/service/glue/waiter/waiter.go @@ -9,10 +9,13 @@ 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 + MLTransformDeleteTimeout = 2 * time.Minute + RegistryDeleteTimeout = 2 * time.Minute + SchemaAvailableTimeout = 2 * time.Minute + SchemaDeleteTimeout = 2 * time.Minute + SchemaVersionAvailableTimeout = 2 * time.Minute + TriggerCreateTimeout = 2 * time.Minute + TriggerDeleteTimeout = 2 * time.Minute ) // MLTransformDeleted waits for an MLTransform to return Deleted @@ -51,6 +54,60 @@ func RegistryDeleted(conn *glue.Glue, registryID string) (*glue.GetRegistryOutpu return nil, err } +// SchemaAvailable waits for a Schema to return Available +func SchemaAvailable(conn *glue.Glue, registryID string) (*glue.GetSchemaOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{glue.SchemaStatusPending}, + Target: []string{glue.SchemaStatusAvailable}, + Refresh: SchemaStatus(conn, registryID), + Timeout: SchemaAvailableTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*glue.GetSchemaOutput); ok { + return output, err + } + + return nil, err +} + +// SchemaDeleted waits for a Schema to return Deleted +func SchemaDeleted(conn *glue.Glue, registryID string) (*glue.GetSchemaOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{glue.SchemaStatusDeleting}, + Target: []string{}, + Refresh: SchemaStatus(conn, registryID), + Timeout: SchemaDeleteTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*glue.GetSchemaOutput); ok { + return output, err + } + + return nil, err +} + +// SchemaVersionAvailable waits for a Schema to return Available +func SchemaVersionAvailable(conn *glue.Glue, registryID string) (*glue.GetSchemaVersionOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{glue.SchemaVersionStatusPending}, + Target: []string{glue.SchemaVersionStatusAvailable}, + Refresh: SchemaVersionStatus(conn, registryID), + Timeout: SchemaVersionAvailableTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*glue.GetSchemaVersionOutput); 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 79d050c6a0a..7f56d5f9c18 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -674,11 +674,12 @@ func Provider() *schema.Provider { "aws_glue_ml_transform": resourceAwsGlueMLTransform(), "aws_glue_partition": resourceAwsGluePartition(), "aws_glue_registry": resourceAwsGlueRegistry(), + "aws_glue_resource_policy": resourceAwsGlueResourcePolicy(), + "aws_glue_schema": resourceAwsGlueSchema(), "aws_glue_security_configuration": resourceAwsGlueSecurityConfiguration(), "aws_glue_trigger": resourceAwsGlueTrigger(), "aws_glue_user_defined_function": resourceAwsGlueUserDefinedFunction(), "aws_glue_workflow": resourceAwsGlueWorkflow(), - "aws_glue_resource_policy": resourceAwsGlueResourcePolicy(), "aws_guardduty_detector": resourceAwsGuardDutyDetector(), "aws_guardduty_filter": resourceAwsGuardDutyFilter(), "aws_guardduty_invite_accepter": resourceAwsGuardDutyInviteAccepter(), diff --git a/aws/resource_aws_glue_schema.go b/aws/resource_aws_glue_schema.go new file mode 100644 index 00000000000..5e3d796289d --- /dev/null +++ b/aws/resource_aws_glue_schema.go @@ -0,0 +1,267 @@ +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 resourceAwsGlueSchema() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsGlueSchemaCreate, + Read: resourceAwsGlueSchemaRead, + Update: resourceAwsGlueSchemaUpdate, + Delete: resourceAwsGlueSchemaDelete, + 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_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateArn, + }, + "registry_name": { + Type: schema.TypeString, + Computed: true, + }, + "latest_schema_version": { + Type: schema.TypeInt, + Computed: true, + }, + "next_schema_version": { + Type: schema.TypeInt, + Computed: true, + }, + "schema_checkpoint": { + Type: schema.TypeInt, + Computed: true, + }, + "compatibility": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(glue.Compatibility_Values(), false), + }, + "data_format": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(glue.DataFormat_Values(), false), + }, + "schema_definition": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 170000), + validation.StringMatch(regexp.MustCompile(`.*\S.*`), ""), + ), + }, + "schema_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 resourceAwsGlueSchemaCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + + input := &glue.CreateSchemaInput{ + SchemaName: aws.String(d.Get("schema_name").(string)), + SchemaDefinition: aws.String(d.Get("schema_definition").(string)), + DataFormat: aws.String(d.Get("data_format").(string)), + Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().GlueTags(), + } + + if v, ok := d.GetOk("registry_arn"); ok { + input.RegistryId = tfglue.CreateAwsGlueRegistryID(v.(string)) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("compatibility"); ok { + input.Compatibility = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating Glue Schema: %s", input) + output, err := conn.CreateSchema(input) + if err != nil { + return fmt.Errorf("error creating Glue Schema: %w", err) + } + d.SetId(aws.StringValue(output.SchemaArn)) + + _, err = waiter.SchemaAvailable(conn, d.Id()) + if err != nil { + return fmt.Errorf("error waiting for Glue Schema (%s) to be Available: %w", d.Id(), err) + } + + return resourceAwsGlueSchemaRead(d, meta) +} + +func resourceAwsGlueSchemaRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + output, err := finder.SchemaByID(conn, d.Id()) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + log.Printf("[WARN] Glue Schema (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("error reading Glue Schema (%s): %w", d.Id(), err) + } + + if output == nil { + log.Printf("[WARN] Glue Schema (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + arn := aws.StringValue(output.SchemaArn) + d.Set("arn", arn) + d.Set("description", output.Description) + d.Set("schema_name", output.SchemaName) + d.Set("compatibility", output.Compatibility) + d.Set("data_format", output.DataFormat) + d.Set("latest_schema_version", output.LatestSchemaVersion) + d.Set("next_schema_version", output.NextSchemaVersion) + d.Set("registry_arn", output.RegistryArn) + d.Set("registry_name", output.RegistryName) + d.Set("schema_checkpoint", output.SchemaCheckpoint) + + tags, err := keyvaluetags.GlueListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for Glue Schema (%s): %w", arn, err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + schemeDefOutput, err := finder.SchemaVersionByID(conn, d.Id()) + if err != nil { + return fmt.Errorf("error reading Glue Schema Definition (%s): %w", d.Id(), err) + } + + d.Set("schema_definition", schemeDefOutput.SchemaDefinition) + + return nil +} + +func resourceAwsGlueSchemaUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + + input := &glue.UpdateSchemaInput{ + SchemaId: tfglue.CreateAwsGlueSchemaID(d.Id()), + SchemaVersionNumber: &glue.SchemaVersionNumber{ + LatestVersion: aws.Bool(true), + }, + } + update := false + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + update = true + } + + if d.HasChange("compatibility") { + input.Compatibility = aws.String(d.Get("compatibility").(string)) + update = true + } + + if update { + log.Printf("[DEBUG] Updating Glue Schema: %#v", input) + _, err := conn.UpdateSchema(input) + if err != nil { + return fmt.Errorf("error updating Glue Schema (%s): %w", d.Id(), err) + } + + _, err = waiter.SchemaAvailable(conn, d.Id()) + if err != nil { + return fmt.Errorf("error waiting for Glue Schema (%s) to be Available: %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) + } + } + + if d.HasChange("schema_definition") { + defInput := &glue.RegisterSchemaVersionInput{ + SchemaId: tfglue.CreateAwsGlueSchemaID(d.Id()), + SchemaDefinition: aws.String(d.Get("schema_definition").(string)), + } + + _, err := conn.RegisterSchemaVersion(defInput) + if err != nil { + return fmt.Errorf("error updating Glue Schema Definition (%s): %w", d.Id(), err) + } + + _, err = waiter.SchemaVersionAvailable(conn, d.Id()) + if err != nil { + return fmt.Errorf("error waiting for Glue Schema Version (%s) to be Available: %w", d.Id(), err) + } + } + + return resourceAwsGlueSchemaRead(d, meta) +} + +func resourceAwsGlueSchemaDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + + log.Printf("[DEBUG] Deleting Glue Schema: %s", d.Id()) + input := &glue.DeleteSchemaInput{ + SchemaId: tfglue.CreateAwsGlueSchemaID(d.Id()), + } + + _, err := conn.DeleteSchema(input) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + return nil + } + return fmt.Errorf("error deleting Glue Schema (%s): %w", d.Id(), err) + } + + _, err = waiter.SchemaDeleted(conn, d.Id()) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + return nil + } + return fmt.Errorf("error waiting for Glue Schema (%s) to be deleted: %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_glue_schema_test.go b/aws/resource_aws_glue_schema_test.go new file mode 100644 index 00000000000..706a5f6d481 --- /dev/null +++ b/aws/resource_aws_glue_schema_test.go @@ -0,0 +1,446 @@ +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_schema", &resource.Sweeper{ + Name: "aws_glue_schema", + F: testSweepGlueSchema, + }) +} + +func testSweepGlueSchema(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.ListSchemas(&glue.ListSchemasInput{}) + if err != nil { + // Some endpoints that do not support Glue Schemas return InternalFailure + if testSweepSkipSweepError(err) || isAWSErr(err, "InternalFailure", "") { + log.Printf("[WARN] Skipping Glue Schema sweep for %s: %s", region, err) + return nil + } + return fmt.Errorf("Error retrieving Glue Schema: %s", err) + } + for _, schema := range listOutput.Schemas { + arn := aws.StringValue(schema.SchemaArn) + r := resourceAwsGlueSchema() + d := r.Data(nil) + d.SetId(arn) + + err := r.Delete(d, client) + if err != nil { + log.Printf("[ERROR] Failed to delete Glue Schema %s: %s", arn, err) + } + } + return nil +} + +func TestAccAWSGlueSchema_basic(t *testing.T) { + var schema glue.GetSchemaOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_schema.test" + registryResourceName := "aws_glue_registry.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueSchema(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueSchemaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueSchemaBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "glue", fmt.Sprintf("schema/%s/%s", rName, rName)), + resource.TestCheckResourceAttr(resourceName, "schema_name", rName), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "compatibility", "NONE"), + resource.TestCheckResourceAttr(resourceName, "data_format", "AVRO"), + resource.TestCheckResourceAttr(resourceName, "schema_checkpoint", "1"), + resource.TestCheckResourceAttr(resourceName, "latest_schema_version", "1"), + resource.TestCheckResourceAttr(resourceName, "next_schema_version", "2"), + resource.TestCheckResourceAttr(resourceName, "schema_definition", "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"int\"}, {\"name\": \"f2\", \"type\": \"string\"} ]}"), + resource.TestCheckResourceAttrPair(resourceName, "registry_name", registryResourceName, "registry_name"), + resource.TestCheckResourceAttrPair(resourceName, "registry_arn", registryResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSGlueSchema_description(t *testing.T) { + var schema glue.GetSchemaOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_schema.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueSchema(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueSchemaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueSchemaDescriptionConfig(rName, "First Description"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "description", "First Description"), + ), + }, + { + Config: testAccAWSGlueSchemaDescriptionConfig(rName, "Second Description"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "description", "Second Description"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSGlueSchema_compatibility(t *testing.T) { + var schema glue.GetSchemaOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_schema.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueSchema(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueSchemaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueSchemaCompatibillityConfig(rName, "DISABLED"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "compatibility", "DISABLED"), + ), + }, + { + Config: testAccAWSGlueSchemaCompatibillityConfig(rName, "FULL"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "compatibility", "FULL"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSGlueSchema_tags(t *testing.T) { + var schema glue.GetSchemaOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_schema.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueSchema(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueSchemaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueSchemaConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSGlueSchemaConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSGlueSchemaConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccAWSGlueSchema_schemaDefUpdated(t *testing.T) { + var schema glue.GetSchemaOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_schema.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueSchema(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueSchemaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueSchemaBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "schema_definition", "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"int\"}, {\"name\": \"f2\", \"type\": \"string\"} ]}"), + resource.TestCheckResourceAttr(resourceName, "latest_schema_version", "1"), + resource.TestCheckResourceAttr(resourceName, "next_schema_version", "2"), + ), + }, + { + Config: testAccAWSGlueSchemaConfigSchemaDefinitionUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + resource.TestCheckResourceAttr(resourceName, "schema_definition", "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"string\"}, {\"name\": \"f2\", \"type\": \"int\"} ]}"), + resource.TestCheckResourceAttr(resourceName, "latest_schema_version", "2"), + resource.TestCheckResourceAttr(resourceName, "next_schema_version", "3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSGlueSchema_disappears(t *testing.T) { + var schema glue.GetSchemaOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_schema.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueSchema(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueSchemaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueSchemaBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + testAccCheckResourceDisappears(testAccProvider, resourceAwsGlueSchema(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSGlueSchema_disappears_registry(t *testing.T) { + var schema glue.GetSchemaOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_schema.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSGlueSchema(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGlueSchemaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlueSchemaBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGlueSchemaExists(resourceName, &schema), + testAccCheckResourceDisappears(testAccProvider, resourceAwsGlueRegistry(), "aws_glue_registry.test"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccPreCheckAWSGlueSchema(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).glueconn + + _, err := conn.ListRegistries(&glue.ListRegistriesInput{}) + + // Some endpoints that do not support Glue Schemas 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 testAccCheckAWSGlueSchemaExists(resourceName string, schema *glue.GetSchemaOutput) 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 Schema ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).glueconn + output, err := finder.SchemaByID(conn, rs.Primary.ID) + if err != nil { + return err + } + + if output == nil { + return fmt.Errorf("Glue Schema (%s) not found", rs.Primary.ID) + } + + if aws.StringValue(output.SchemaArn) == rs.Primary.ID { + *schema = *output + return nil + } + + return fmt.Errorf("Glue Schema (%s) not found", rs.Primary.ID) + } +} + +func testAccCheckAWSGlueSchemaDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_glue_schema" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).glueconn + output, err := finder.SchemaByID(conn, rs.Primary.ID) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + return nil + } + + } + + if output != nil && aws.StringValue(output.SchemaArn) == rs.Primary.ID { + return fmt.Errorf("Glue Schema %s still exists", rs.Primary.ID) + } + + return err + } + + return nil +} + +func testAccAWSGlueSchemaBase(rName string) string { + return fmt.Sprintf(` +resource "aws_glue_registry" "test" { + registry_name = %[1]q +} +`, rName) +} + +func testAccAWSGlueSchemaDescriptionConfig(rName, description string) string { + return testAccAWSGlueSchemaBase(rName) + fmt.Sprintf(` +resource "aws_glue_schema" "test" { + schema_name = %[1]q + registry_arn = aws_glue_registry.test.arn + data_format = "AVRO" + compatibility = "NONE" + description = %[2]q + schema_definition = "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"int\"}, {\"name\": \"f2\", \"type\": \"string\"} ]}" +} +`, rName, description) +} + +func testAccAWSGlueSchemaCompatibillityConfig(rName, compat string) string { + return testAccAWSGlueSchemaBase(rName) + fmt.Sprintf(` +resource "aws_glue_schema" "test" { + schema_name = %[1]q + registry_arn = aws_glue_registry.test.arn + data_format = "AVRO" + compatibility = %[2]q + schema_definition = "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"int\"}, {\"name\": \"f2\", \"type\": \"string\"} ]}" +} +`, rName, compat) +} + +func testAccAWSGlueSchemaBasicConfig(rName string) string { + return testAccAWSGlueSchemaBase(rName) + fmt.Sprintf(` +resource "aws_glue_schema" "test" { + schema_name = %[1]q + registry_arn = aws_glue_registry.test.arn + data_format = "AVRO" + compatibility = "NONE" + schema_definition = "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"int\"}, {\"name\": \"f2\", \"type\": \"string\"} ]}" +} +`, rName) +} + +func testAccAWSGlueSchemaConfigTags1(rName, tagKey1, tagValue1 string) string { + return testAccAWSGlueSchemaBase(rName) + fmt.Sprintf(` +resource "aws_glue_schema" "test" { + schema_name = %[1]q + registry_arn = aws_glue_registry.test.arn + data_format = "AVRO" + compatibility = "NONE" + schema_definition = "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"int\"}, {\"name\": \"f2\", \"type\": \"string\"} ]}" + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSGlueSchemaConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return testAccAWSGlueSchemaBase(rName) + fmt.Sprintf(` +resource "aws_glue_schema" "test" { + schema_name = %[1]q + registry_arn = aws_glue_registry.test.arn + data_format = "AVRO" + compatibility = "NONE" + schema_definition = "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"int\"}, {\"name\": \"f2\", \"type\": \"string\"} ]}" + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccAWSGlueSchemaConfigSchemaDefinitionUpdated(rName string) string { + return testAccAWSGlueSchemaBase(rName) + fmt.Sprintf(` +resource "aws_glue_schema" "test" { + schema_name = %[1]q + registry_arn = aws_glue_registry.test.arn + data_format = "AVRO" + compatibility = "NONE" + schema_definition = "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"string\"}, {\"name\": \"f2\", \"type\": \"int\"} ]}" +} +`, rName) +} diff --git a/website/docs/r/glue_schema.html.markdown b/website/docs/r/glue_schema.html.markdown new file mode 100644 index 00000000000..f2f53fb1ec6 --- /dev/null +++ b/website/docs/r/glue_schema.html.markdown @@ -0,0 +1,54 @@ +--- +subcategory: "Glue" +layout: "aws" +page_title: "AWS: aws_glue_schema" +description: |- + Provides a Glue Schema resource. +--- + +# Resource: aws_glue_schema + +Provides a Glue Schema resource. + +## Example Usage + +```hcl +resource "aws_glue_schema" "example" { + schema_name = "example" + registry_arn = aws_glue_registry.test.arn + data_format = "AVRO" + compatibility = "NONE" + schema_definition = "{\"type\": \"record\", \"name\": \"r1\", \"fields\": [ {\"name\": \"f1\", \"type\": \"int\"}, {\"name\": \"f2\", \"type\": \"string\"} ]}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `schema_name` – (Required) The Name of the schema. +* `registry_arn` - (Required) The ARN of the Glue Registry to create the schema in. +* `data_format` - (Required) The data format of the schema definition. Currently only `AVRO` is supported. +* `compatibility` - (Required) The compatibility mode of the schema. Values values are: `NONE`, `DISABLED`, `BACKWARD`, `BACKWARD_ALL`, `FORWARD`, `FORWARD_ALL`, `FULL`, and `FULL_ALL`. +* `schema_definition` - (Required) The schema definition using the `data_format` setting for `schema_name`. +* `description` – (Optional) A description of the schema. +* `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 the schema. +* `id` - Amazon Resource Name (ARN) of the schema. +* `registry_name` - The name of the Glue Registry. +* `latest_schema_version` - The latest version of the schema associated with the returned schema definition. +* `next_schema_version` - The next version of the schema associated with the returned schema definition. +* `schema_checkpoint` - The version number of the checkpoint (the last time the compatibility mode was changed). + +## Import + +Glue Registries can be imported using `arn`, e.g. + +``` +$ terraform import aws_glue_schema.example arn:aws:glue:us-west-2:123456789012:schema/example/example +```