diff --git a/aws/provider.go b/aws/provider.go index 8bc6f07a5b48..8988be2beb4e 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -627,6 +627,7 @@ func Provider() *schema.Provider { "aws_glue_job": resourceAwsGlueJob(), "aws_glue_security_configuration": resourceAwsGlueSecurityConfiguration(), "aws_glue_trigger": resourceAwsGlueTrigger(), + "aws_glue_user_defined_function": resourceAwsGlueUserDefinedFunction(), "aws_glue_workflow": resourceAwsGlueWorkflow(), "aws_guardduty_detector": resourceAwsGuardDutyDetector(), "aws_guardduty_publishing_destination": resourceAwsGuardDutyPublishingDestination(), diff --git a/aws/resource_aws_glue_user_defined_function.go b/aws/resource_aws_glue_user_defined_function.go new file mode 100644 index 000000000000..5ddaa773b85d --- /dev/null +++ b/aws/resource_aws_glue_user_defined_function.go @@ -0,0 +1,262 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "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" +) + +func resourceAwsGlueUserDefinedFunction() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsGlueUserDefinedFunctionCreate, + Read: resourceAwsGlueUserDefinedFunctionRead, + Update: resourceAwsGlueUserDefinedFunctionUpdate, + Delete: resourceAwsGlueUserDefinedFunctionDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "catalog_id": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + }, + "database_name": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + "name": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "class_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "owner_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "owner_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(glue.PrincipalType_Values(), false), + }, + "resource_uris": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1000, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(glue.ResourceType_Values(), false), + }, + "uri": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 1024), + }, + }, + }, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsGlueUserDefinedFunctionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + catalogID := createAwsGlueCatalogID(d, meta.(*AWSClient).accountid) + dbName := d.Get("database_name").(string) + funcName := d.Get("name").(string) + + input := &glue.CreateUserDefinedFunctionInput{ + CatalogId: aws.String(catalogID), + DatabaseName: aws.String(dbName), + FunctionInput: expandAwsGlueUserDefinedFunctionInput(d), + } + + _, err := conn.CreateUserDefinedFunction(input) + if err != nil { + return fmt.Errorf("error creating Glue User Defined Function: %w", err) + } + + d.SetId(fmt.Sprintf("%s:%s:%s", catalogID, dbName, funcName)) + + return resourceAwsGlueUserDefinedFunctionRead(d, meta) +} + +func resourceAwsGlueUserDefinedFunctionUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + + catalogID, dbName, funcName, err := readAwsGlueUDFID(d.Id()) + if err != nil { + return err + } + + input := &glue.UpdateUserDefinedFunctionInput{ + CatalogId: aws.String(catalogID), + DatabaseName: aws.String(dbName), + FunctionName: aws.String(funcName), + FunctionInput: expandAwsGlueUserDefinedFunctionInput(d), + } + + if _, err := conn.UpdateUserDefinedFunction(input); err != nil { + return fmt.Errorf("error updating Glue User Defined Function (%s): %w", d.Id(), err) + } + + return resourceAwsGlueUserDefinedFunctionRead(d, meta) +} + +func resourceAwsGlueUserDefinedFunctionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + + catalogID, dbName, funcName, err := readAwsGlueUDFID(d.Id()) + if err != nil { + return err + } + + input := &glue.GetUserDefinedFunctionInput{ + CatalogId: aws.String(catalogID), + DatabaseName: aws.String(dbName), + FunctionName: aws.String(funcName), + } + + out, err := conn.GetUserDefinedFunction(input) + if err != nil { + + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + log.Printf("[WARN] Glue User Defined Function (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + return fmt.Errorf("error reading Glue User Defined Function: %w", err) + } + + udf := out.UserDefinedFunction + + udfArn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "glue", + Region: meta.(*AWSClient).region, + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("userDefinedFunction/%s/%s", dbName, aws.StringValue(udf.FunctionName)), + }.String() + + d.Set("arn", udfArn) + d.Set("name", udf.FunctionName) + d.Set("catalog_id", catalogID) + d.Set("database_name", dbName) + d.Set("owner_type", udf.OwnerType) + d.Set("owner_name", udf.OwnerName) + d.Set("class_name", udf.ClassName) + if udf.CreateTime != nil { + d.Set("create_time", udf.CreateTime.Format(time.RFC3339)) + } + if err := d.Set("resource_uris", flattenAwsGlueUserDefinedFunctionResourceUri(udf.ResourceUris)); err != nil { + return err + } + + return nil +} + +func resourceAwsGlueUserDefinedFunctionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).glueconn + catalogID, dbName, funcName, err := readAwsGlueUDFID(d.Id()) + if err != nil { + return err + } + + log.Printf("[DEBUG] Glue User Defined Function: %s", d.Id()) + _, err = conn.DeleteUserDefinedFunction(&glue.DeleteUserDefinedFunctionInput{ + CatalogId: aws.String(catalogID), + DatabaseName: aws.String(dbName), + FunctionName: aws.String(funcName), + }) + if err != nil { + return fmt.Errorf("error deleting Glue User Defined Function: %w", err) + } + return nil +} + +func readAwsGlueUDFID(id string) (catalogID string, dbName string, funcName string, err error) { + idParts := strings.Split(id, ":") + if len(idParts) != 3 { + return "", "", "", fmt.Errorf("unexpected format of ID (%q), expected CATALOG-ID:DATABASE-NAME:FUNCTION-NAME", id) + } + return idParts[0], idParts[1], idParts[2], nil +} + +func expandAwsGlueUserDefinedFunctionInput(d *schema.ResourceData) *glue.UserDefinedFunctionInput { + + udf := &glue.UserDefinedFunctionInput{ + ClassName: aws.String(d.Get("class_name").(string)), + FunctionName: aws.String(d.Get("name").(string)), + OwnerName: aws.String(d.Get("owner_name").(string)), + OwnerType: aws.String(d.Get("owner_type").(string)), + } + + if v, ok := d.GetOk("resource_uris"); ok && v.(*schema.Set).Len() > 0 { + udf.ResourceUris = expandAwsGlueUserDefinedFunctionResourceUri(d.Get("resource_uris").(*schema.Set)) + } + + return udf +} + +func expandAwsGlueUserDefinedFunctionResourceUri(conf *schema.Set) []*glue.ResourceUri { + result := make([]*glue.ResourceUri, 0, conf.Len()) + + for _, r := range conf.List() { + uriRaw, ok := r.(map[string]interface{}) + + if !ok { + continue + } + + uri := &glue.ResourceUri{ + ResourceType: aws.String(uriRaw["resource_type"].(string)), + Uri: aws.String(uriRaw["uri"].(string)), + } + + result = append(result, uri) + } + + return result +} + +func flattenAwsGlueUserDefinedFunctionResourceUri(uris []*glue.ResourceUri) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(uris)) + + for _, i := range uris { + l := map[string]interface{}{ + "resource_type": aws.StringValue(i.ResourceType), + "uri": aws.StringValue(i.Uri), + } + + result = append(result, l) + } + return result +} diff --git a/aws/resource_aws_glue_user_defined_function_test.go b/aws/resource_aws_glue_user_defined_function_test.go new file mode 100644 index 000000000000..4b1e58963704 --- /dev/null +++ b/aws/resource_aws_glue_user_defined_function_test.go @@ -0,0 +1,249 @@ +package aws + +import ( + "fmt" + "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" +) + +func TestAccAWSGlueUserDefinedFunction_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + updated := "test" + resourceName := "aws_glue_user_defined_function.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGlueUDFDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGlueUserDefinedFunctionBasicConfig(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGlueUserDefinedFunctionExists(resourceName), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "glue", fmt.Sprintf("userDefinedFunction/%s/%s", rName, rName)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "class_name", rName), + resource.TestCheckResourceAttr(resourceName, "owner_name", rName), + resource.TestCheckResourceAttr(resourceName, "owner_type", "GROUP"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccGlueUserDefinedFunctionBasicConfig(rName, updated), + Check: resource.ComposeTestCheckFunc( + testAccCheckGlueUserDefinedFunctionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "class_name", updated), + resource.TestCheckResourceAttr(resourceName, "owner_name", updated), + resource.TestCheckResourceAttr(resourceName, "owner_type", "GROUP"), + ), + }, + }, + }) +} + +func TestAccAWSGlueUserDefinedFunction_resource_uri(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_user_defined_function.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGlueUDFDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGlueUserDefinedFunctionResourceURIConfig1(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGlueUserDefinedFunctionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "resource_uris.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccGlueUserDefinedFunctionResourceURIConfig2(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGlueUserDefinedFunctionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "resource_uris.#", "2"), + ), + }, + { + Config: testAccGlueUserDefinedFunctionResourceURIConfig1(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGlueUserDefinedFunctionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "resource_uris.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSGlueUserDefinedFunction_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_glue_user_defined_function.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGlueUDFDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGlueUserDefinedFunctionBasicConfig(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGlueUserDefinedFunctionExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsGlueUserDefinedFunction(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckGlueUDFDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).glueconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_glue_user_defined_function" { + continue + } + + catalogId, dbName, funcName, err := readAwsGlueUDFID(rs.Primary.ID) + if err != nil { + return err + } + + input := &glue.GetUserDefinedFunctionInput{ + CatalogId: aws.String(catalogId), + DatabaseName: aws.String(dbName), + FunctionName: aws.String(funcName), + } + if _, err := conn.GetUserDefinedFunction(input); err != nil { + //Verify the error is what we want + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + continue + } + + return err + } + return fmt.Errorf("still exists") + } + return nil +} + +func testAccCheckGlueUserDefinedFunctionExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + catalogId, dbName, funcName, err := readAwsGlueUDFID(rs.Primary.ID) + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).glueconn + out, err := conn.GetUserDefinedFunction(&glue.GetUserDefinedFunctionInput{ + CatalogId: aws.String(catalogId), + DatabaseName: aws.String(dbName), + FunctionName: aws.String(funcName), + }) + + if err != nil { + return err + } + + if out.UserDefinedFunction == nil { + return fmt.Errorf("No Glue User Defined Function Found") + } + + if *out.UserDefinedFunction.FunctionName != funcName { + return fmt.Errorf("Glue UDF Mismatch - existing: %q, state: %q", + *out.UserDefinedFunction.FunctionName, funcName) + } + + return nil + } +} + +func testAccGlueUserDefinedFunctionBasicConfig(rName string, name string) string { + return fmt.Sprintf(` +resource "aws_glue_catalog_database" "test" { + name = %[1]q +} + +resource "aws_glue_user_defined_function" "test" { + name = %[1]q + catalog_id = aws_glue_catalog_database.test.catalog_id + database_name = aws_glue_catalog_database.test.name + class_name = %[2]q + owner_name = %[2]q + owner_type = "GROUP" +} +`, rName, name) +} + +func testAccGlueUserDefinedFunctionResourceURIConfig1(rName string) string { + return fmt.Sprintf(` +resource "aws_glue_catalog_database" "test" { + name = %[1]q +} + +resource "aws_glue_user_defined_function" "test" { + name = %[1]q + catalog_id = aws_glue_catalog_database.test.catalog_id + database_name = aws_glue_catalog_database.test.name + class_name = %[1]q + owner_name = %[1]q + owner_type = "GROUP" + + resource_uris { + resource_type = "ARCHIVE" + uri = %[1]q + } +} +`, rName) +} + +func testAccGlueUserDefinedFunctionResourceURIConfig2(rName string) string { + return fmt.Sprintf(` +resource "aws_glue_catalog_database" "test" { + name = %[1]q +} + +resource "aws_glue_user_defined_function" "test" { + name = %[1]q + catalog_id = aws_glue_catalog_database.test.catalog_id + database_name = aws_glue_catalog_database.test.name + class_name = %[1]q + owner_name = %[1]q + owner_type = "GROUP" + + resource_uris { + resource_type = "ARCHIVE" + uri = %[1]q + } + + resource_uris { + resource_type = "JAR" + uri = %[1]q + } +} +`, rName) +} diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index e37cb2664f2a..2942f93fc200 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -265,6 +265,7 @@ for more information about connecting to alternate AWS endpoints or AWS compatib - [`aws_glue_crawler` resource](/docs/providers/aws/r/glue_crawler.html) - [`aws_glue_job` resource](/docs/providers/aws/r/glue_job.html) - [`aws_glue_trigger` resource](/docs/providers/aws/r/glue_trigger.html) + - [`aws_glue_user_defined_function` resource](/docs/providers/aws/r/glue_user_defined_function.html) - [`aws_guardduty_detector` resource](/docs/providers/aws/r/guardduty_detector.html) - [`aws_guardduty_ipset` resource](/docs/providers/aws/r/guardduty_ipset.html) - [`aws_guardduty_threatintelset` resource](/docs/providers/aws/r/guardduty_threatintelset.html) diff --git a/website/docs/r/glue_user_defined_function.html.markdown b/website/docs/r/glue_user_defined_function.html.markdown new file mode 100644 index 000000000000..bd50721482f1 --- /dev/null +++ b/website/docs/r/glue_user_defined_function.html.markdown @@ -0,0 +1,66 @@ +--- +subcategory: "Glue" +layout: "aws" +page_title: "AWS: aws_glue_user_defined_function" +description: |- + Provides a Glue User Defined Function. +--- + +# Resource: aws_glue_user_defined_function + +Provides a Glue User Defined Function Resource. + +## Example Usage + +```hcl +resource "aws_glue_catalog_database" "example" { + name = "my_database" +} + +resource "aws_glue_user_defined_function" "example" { + name = "my_func" + catalog_id = aws_glue_catalog_database.example.catalog_id + database_name = aws_glue_catalog_database.example.name + class_name = "class" + owner_name = "owner" + owner_type = "GROUP" + + resource_uris { + resource_type = "ARCHIVE" + uri = "uri" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the function. +* `catalog_id` - (Optional) ID of the Glue Catalog to create the function in. If omitted, this defaults to the AWS Account ID. +* `database_name` - (Required) The name of the Database to create the Function. +* `class_name` - (Required) The Java class that contains the function code. +* `owner_name` - (Required) The owner of the function. +* `owner_type` - (Required) The owner type. can be one of `USER`, `ROLE`, and `GROUP`. +* `resource_uris` - (Optional) The configuration block for Resource URIs. See [resource uris](#resource-uris) below for more details. + +### Resource URIs + +* `resource_type` - (Required) The type of the resource. can be one of `JAR`, `FILE`, and `ARCHIVE`. +* `uri` - (Required) The URI for accessing the resource. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id`- The id of the Glue User Defined Function. +* `arn`- The ARN of the Glue User Defined Function. +* `create_date`- The time at which the function was created. + +## Import + +Glue User Defined Functions can be imported using the `catalog_id:database_name:function_name`. If you have not set a Catalog ID specify the AWS Account ID that the database is in, e.g. + +``` +$ terraform import aws_glue_user_defined_function.func 123456789012:my_database:my_func +```