diff --git a/.changelog/22411.txt b/.changelog/22411.txt new file mode 100644 index 00000000000..13a315f5ffa --- /dev/null +++ b/.changelog/22411.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_appsync_datasource: Add `delta_sync_config` and `versioned` to the `dynamodb_config` configuration block +``` + +```release-note:enhancement +resource/aws_appsync_datasource: Add `authorization_config` attribute to the `http_config` configuration block +``` + +```release-note:enhancement +resource/aws_appsync_datasource: Add plan time validation for `service_role_arn` and `lambda_config.function_arn` +``` + +```release-note:enhancement +resource/aws_appsync_datasource: Add `relational_database_config` argument +``` diff --git a/internal/service/appsync/appsync_test.go b/internal/service/appsync/appsync_test.go index d7d52489826..6fb8ddce4fc 100644 --- a/internal/service/appsync/appsync_test.go +++ b/internal/service/appsync/appsync_test.go @@ -20,8 +20,11 @@ func TestAccAppSync_serial(t *testing.T) { "type": testAccAppSyncDataSource_type, "Type_dynamoDB": testAccAppSyncDataSource_Type_dynamoDB, "Type_http": testAccAppSyncDataSource_Type_http, + "Type_http_auth": testAccAppSyncDataSource_Type_http_auth, "Type_lambda": testAccAppSyncDataSource_Type_lambda, "Type_none": testAccAppSyncDataSource_Type_none, + "Type_rdbms": testAccAppsyncDatasource_Type_RelationalDatabase, + "Type_rdbms_options": testAccAppsyncDatasource_Type_RelationalDatabaseWithOptions, }, "GraphQLAPI": { "basic": testAccAppSyncGraphQLAPI_basic, diff --git a/internal/service/appsync/datasource.go b/internal/service/appsync/datasource.go index f7133d97c78..976defb9105 100644 --- a/internal/service/appsync/datasource.go +++ b/internal/service/appsync/datasource.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/verify" ) func ResourceDataSource() *schema.Resource { @@ -30,26 +31,14 @@ func ResourceDataSource() *schema.Resource { Required: true, }, "name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - if !regexp.MustCompile(`[_A-Za-z][_0-9A-Za-z]*`).MatchString(value) { - errors = append(errors, fmt.Errorf("%q must match [_A-Za-z][_0-9A-Za-z]*", k)) - } - return - }, + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[_A-Za-z][_0-9A-Za-z]*`), "must match [_A-Za-z][_0-9A-Za-z]*"), }, "type": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{ - appsync.DataSourceTypeAwsLambda, - appsync.DataSourceTypeAmazonDynamodb, - appsync.DataSourceTypeAmazonElasticsearch, - appsync.DataSourceTypeHttp, - appsync.DataSourceTypeNone, - }, true), + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(appsync.DataSourceType_Values(), true), StateFunc: func(v interface{}) string { return strings.ToUpper(v.(string)) }, @@ -77,9 +66,34 @@ func ResourceDataSource() *schema.Resource { Type: schema.TypeBool, Optional: true, }, + "versioned": { + Type: schema.TypeBool, + Optional: true, + }, + "delta_sync_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "base_table_ttl": { + Type: schema.TypeInt, + Optional: true, + }, + "delta_sync_table_name": { + Type: schema.TypeString, + Required: true, + }, + "delta_sync_table_ttl": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, }, }, - ConflictsWith: []string{"elasticsearch_config", "http_config", "lambda_config"}, + ConflictsWith: []string{"elasticsearch_config", "http_config", "lambda_config", "relational_database_config"}, }, "elasticsearch_config": { Type: schema.TypeList, @@ -110,9 +124,41 @@ func ResourceDataSource() *schema.Resource { Type: schema.TypeString, Required: true, }, + "authorization_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "authorization_type": { + Type: schema.TypeString, + Optional: true, + Default: appsync.AuthorizationTypeAwsIam, + ValidateFunc: validation.StringInSlice(appsync.AuthorizationType_Values(), true), + }, + "aws_iam_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "signing_region": { + Type: schema.TypeString, + Optional: true, + }, + "signing_service_name": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, }, }, - ConflictsWith: []string{"dynamodb_config", "elasticsearch_config", "lambda_config"}, + ConflictsWith: []string{"dynamodb_config", "elasticsearch_config", "lambda_config", "relational_database_config"}, }, "lambda_config": { Type: schema.TypeList, @@ -121,16 +167,65 @@ func ResourceDataSource() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "function_arn": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, }, }, }, - ConflictsWith: []string{"dynamodb_config", "elasticsearch_config", "http_config"}, + ConflictsWith: []string{"dynamodb_config", "elasticsearch_config", "http_config", "relational_database_config"}, }, - "service_role_arn": { - Type: schema.TypeString, + "relational_database_config": { + Type: schema.TypeList, Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source_type": { + Type: schema.TypeString, + Optional: true, + Default: appsync.RelationalDatabaseSourceTypeRdsHttpEndpoint, + ValidateFunc: validation.StringInSlice(appsync.RelationalDatabaseSourceType_Values(), true), + }, + "http_endpoint_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "db_cluster_identifier": { + Type: schema.TypeString, + Required: true, + }, + "aws_secret_store_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + "database_name": { + Type: schema.TypeString, + Optional: true, + }, + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "schema": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + ConflictsWith: []string{"dynamodb_config", "elasticsearch_config", "http_config", "lambda_config"}, + }, + "service_role_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, }, "arn": { Type: schema.TypeString, @@ -174,9 +269,13 @@ func resourceDataSourceCreate(d *schema.ResourceData, meta interface{}) error { input.ServiceRoleArn = aws.String(v.(string)) } + if v, ok := d.GetOk("relational_database_config"); ok { + input.RelationalDatabaseConfig = expandAppsyncRelationalDatabaseDataSourceConfig(v.([]interface{}), region) + } + _, err := conn.CreateDataSource(input) if err != nil { - return err + return fmt.Errorf("error creating Appsync Datasource: %w", err) } d.SetId(d.Get("api_id").(string) + "-" + d.Get("name").(string)) @@ -208,29 +307,35 @@ func resourceDataSourceRead(d *schema.ResourceData, meta interface{}) error { return err } + dataSource := resp.DataSource + d.Set("api_id", apiID) - d.Set("arn", resp.DataSource.DataSourceArn) - d.Set("description", resp.DataSource.Description) + d.Set("arn", dataSource.DataSourceArn) + d.Set("description", dataSource.Description) + + if err := d.Set("dynamodb_config", flattenAppsyncDynamodbDataSourceConfig(dataSource.DynamodbConfig)); err != nil { + return fmt.Errorf("error setting dynamodb_config: %w", err) + } - if err := d.Set("dynamodb_config", flattenAppsyncDynamodbDataSourceConfig(resp.DataSource.DynamodbConfig)); err != nil { - return fmt.Errorf("error setting dynamodb_config: %s", err) + if err := d.Set("elasticsearch_config", flattenAppsyncElasticsearchDataSourceConfig(dataSource.ElasticsearchConfig)); err != nil { + return fmt.Errorf("error setting elasticsearch_config: %w", err) } - if err := d.Set("elasticsearch_config", flattenAppsyncElasticsearchDataSourceConfig(resp.DataSource.ElasticsearchConfig)); err != nil { - return fmt.Errorf("error setting elasticsearch_config: %s", err) + if err := d.Set("http_config", flattenAppsyncHTTPDataSourceConfig(dataSource.HttpConfig)); err != nil { + return fmt.Errorf("error setting http_config: %w", err) } - if err := d.Set("http_config", flattenAppsyncHTTPDataSourceConfig(resp.DataSource.HttpConfig)); err != nil { - return fmt.Errorf("error setting http_config: %s", err) + if err := d.Set("lambda_config", flattenAppsyncLambdaDataSourceConfig(dataSource.LambdaConfig)); err != nil { + return fmt.Errorf("error setting lambda_config: %w", err) } - if err := d.Set("lambda_config", flattenAppsyncLambdaDataSourceConfig(resp.DataSource.LambdaConfig)); err != nil { - return fmt.Errorf("error setting lambda_config: %s", err) + if err := d.Set("relational_database_config", flattenAppsyncRelationalDatabaseDataSourceConfig(dataSource.RelationalDatabaseConfig)); err != nil { + return fmt.Errorf("error setting relational_database_config: %w", err) } - d.Set("name", resp.DataSource.Name) - d.Set("service_role_arn", resp.DataSource.ServiceRoleArn) - d.Set("type", resp.DataSource.Type) + d.Set("name", dataSource.Name) + d.Set("service_role_arn", dataSource.ServiceRoleArn) + d.Set("type", dataSource.Type) return nil } @@ -275,6 +380,10 @@ func resourceDataSourceUpdate(d *schema.ResourceData, meta interface{}) error { input.ServiceRoleArn = aws.String(v.(string)) } + if v, ok := d.GetOk("relational_database_config"); ok { + input.RelationalDatabaseConfig = expandAppsyncRelationalDatabaseDataSourceConfig(v.([]interface{}), region) + } + _, err = conn.UpdateDataSource(input) if err != nil { return err @@ -335,6 +444,38 @@ func expandAppsyncDynamodbDataSourceConfig(l []interface{}, currentRegion string result.UseCallerCredentials = aws.Bool(v.(bool)) } + if v, ok := configured["versioned"]; ok { + result.Versioned = aws.Bool(v.(bool)) + } + + if v, ok := configured["delta_sync_config"].([]interface{}); ok && len(v) > 0 { + result.DeltaSyncConfig = expandAppsyncDynamodbDataSourceDeltaSyncConfig(v) + } + + return result +} + +func expandAppsyncDynamodbDataSourceDeltaSyncConfig(l []interface{}) *appsync.DeltaSyncConfig { + if len(l) == 0 || l[0] == nil { + return nil + } + + configured := l[0].(map[string]interface{}) + + result := &appsync.DeltaSyncConfig{} + + if v, ok := configured["base_table_ttl"].(int); ok { + result.BaseTableTTL = aws.Int64(int64(v)) + } + + if v, ok := configured["delta_sync_table_ttl"].(int); ok { + result.DeltaSyncTableTTL = aws.Int64(int64(v)) + } + + if v, ok := configured["delta_sync_table_name"].(string); ok { + result.DeltaSyncTableName = aws.String(v) + } + return result } @@ -352,6 +493,36 @@ func flattenAppsyncDynamodbDataSourceConfig(config *appsync.DynamodbDataSourceCo result["use_caller_credentials"] = aws.BoolValue(config.UseCallerCredentials) } + if config.Versioned != nil { + result["versioned"] = aws.BoolValue(config.Versioned) + } + + if config.DeltaSyncConfig != nil { + result["delte_sync_config"] = flattenAppsyncDynamodbDataSourceDeltaSyncConfig(config.DeltaSyncConfig) + } + + return []map[string]interface{}{result} +} + +func flattenAppsyncDynamodbDataSourceDeltaSyncConfig(config *appsync.DeltaSyncConfig) []map[string]interface{} { + if config == nil { + return nil + } + + result := map[string]interface{}{} + + if config.DeltaSyncTableName != nil { + result["delta_sync_table_name"] = aws.StringValue(config.DeltaSyncTableName) + } + + if config.BaseTableTTL != nil { + result["base_table_ttl"] = aws.Int64Value(config.BaseTableTTL) + } + + if config.DeltaSyncTableTTL != nil { + result["delta_sync_table_ttl"] = aws.Int64Value(config.DeltaSyncTableTTL) + } + return []map[string]interface{}{result} } @@ -398,6 +569,10 @@ func expandAppsyncHTTPDataSourceConfig(l []interface{}) *appsync.HttpDataSourceC Endpoint: aws.String(configured["endpoint"].(string)), } + if v, ok := configured["authorization_config"].([]interface{}); ok && len(v) > 0 { + result.AuthorizationConfig = expandAppsyncHTTPDataSourceAuthorizationConfig(v) + } + return result } @@ -410,6 +585,77 @@ func flattenAppsyncHTTPDataSourceConfig(config *appsync.HttpDataSourceConfig) [] "endpoint": aws.StringValue(config.Endpoint), } + if config.AuthorizationConfig != nil { + result["authorization_config"] = flattenAppsyncHTTPDataSourceAuthorizationConfig(config.AuthorizationConfig) + } + + return []map[string]interface{}{result} +} + +func expandAppsyncHTTPDataSourceAuthorizationConfig(l []interface{}) *appsync.AuthorizationConfig { + if len(l) == 0 || l[0] == nil { + return nil + } + + configured := l[0].(map[string]interface{}) + + result := &appsync.AuthorizationConfig{ + AuthorizationType: aws.String(configured["authorization_type"].(string)), + } + + if v, ok := configured["aws_iam_config"].([]interface{}); ok && len(v) > 0 { + result.AwsIamConfig = expandAppsyncHTTPDataSourceAwsIamConfig(v) + } + + return result +} + +func flattenAppsyncHTTPDataSourceAuthorizationConfig(config *appsync.AuthorizationConfig) []map[string]interface{} { + if config == nil { + return nil + } + + result := map[string]interface{}{ + "authorization_type": aws.StringValue(config.AuthorizationType), + } + + if config.AwsIamConfig != nil { + result["aws_iam_config"] = flattenAppsyncHTTPDataSourceAwsIamConfig(config.AwsIamConfig) + } + + return []map[string]interface{}{result} +} + +func expandAppsyncHTTPDataSourceAwsIamConfig(l []interface{}) *appsync.AwsIamConfig { + if len(l) == 0 || l[0] == nil { + return nil + } + + configured := l[0].(map[string]interface{}) + + result := &appsync.AwsIamConfig{} + + if v, ok := configured["signing_region"].(string); ok && v != "" { + result.SigningRegion = aws.String(v) + } + + if v, ok := configured["signing_service_name"].(string); ok && v != "" { + result.SigningServiceName = aws.String(v) + } + + return result +} + +func flattenAppsyncHTTPDataSourceAwsIamConfig(config *appsync.AwsIamConfig) []map[string]interface{} { + if config == nil { + return nil + } + + result := map[string]interface{}{ + "signing_region": aws.StringValue(config.SigningRegion), + "signing_service_name": aws.StringValue(config.SigningServiceName), + } + return []map[string]interface{}{result} } @@ -438,3 +684,95 @@ func flattenAppsyncLambdaDataSourceConfig(config *appsync.LambdaDataSourceConfig return []map[string]interface{}{result} } + +func expandAppsyncRelationalDatabaseDataSourceConfig(l []interface{}, currentRegion string) *appsync.RelationalDatabaseDataSourceConfig { + if len(l) == 0 || l[0] == nil { + return nil + } + + configured := l[0].(map[string]interface{}) + + result := &appsync.RelationalDatabaseDataSourceConfig{ + RelationalDatabaseSourceType: aws.String(configured["source_type"].(string)), + RdsHttpEndpointConfig: expandAppsyncRdsHttpEndpointConfig(configured["http_endpoint_config"].([]interface{}), currentRegion), + } + + return result +} + +func flattenAppsyncRelationalDatabaseDataSourceConfig(config *appsync.RelationalDatabaseDataSourceConfig) []map[string]interface{} { + if config == nil { + return nil + } + + result := map[string]interface{}{ + "source_type": aws.StringValue(config.RelationalDatabaseSourceType), + "http_endpoint_config": flattenAppsyncRdsHTTPEndpointConfig(config.RdsHttpEndpointConfig), + } + + return []map[string]interface{}{result} +} + +func expandAppsyncRdsHttpEndpointConfig(l []interface{}, currentRegion string) *appsync.RdsHttpEndpointConfig { + if len(l) == 0 || l[0] == nil { + return nil + } + + configured := l[0].(map[string]interface{}) + + result := &appsync.RdsHttpEndpointConfig{ + AwsRegion: aws.String(currentRegion), + } + + if v, ok := configured["region"]; ok && v.(string) != "" { + result.AwsRegion = aws.String(v.(string)) + } + + if v, ok := configured["aws_secret_store_arn"]; ok && v.(string) != "" { + result.AwsSecretStoreArn = aws.String(v.(string)) + } + + if v, ok := configured["database_name"]; ok && v.(string) != "" { + result.DatabaseName = aws.String(v.(string)) + } + + if v, ok := configured["db_cluster_identifier"]; ok && v.(string) != "" { + result.DbClusterIdentifier = aws.String(v.(string)) + } + + if v, ok := configured["schema"]; ok && v.(string) != "" { + result.Schema = aws.String(v.(string)) + } + + return result +} + +func flattenAppsyncRdsHTTPEndpointConfig(config *appsync.RdsHttpEndpointConfig) []map[string]interface{} { + if config == nil { + return nil + } + + result := map[string]interface{}{} + + if config.AwsRegion != nil { + result["region"] = aws.StringValue(config.AwsRegion) + } + + if config.AwsSecretStoreArn != nil { + result["aws_secret_store_arn"] = aws.StringValue(config.AwsSecretStoreArn) + } + + if config.DatabaseName != nil { + result["database_name"] = aws.StringValue(config.DatabaseName) + } + + if config.DbClusterIdentifier != nil { + result["db_cluster_identifier"] = aws.StringValue(config.DbClusterIdentifier) + } + + if config.Schema != nil { + result["schema"] = aws.StringValue(config.Schema) + } + + return []map[string]interface{}{result} +} diff --git a/internal/service/appsync/datasource_test.go b/internal/service/appsync/datasource_test.go index 9bf6a9407a2..da358be02a4 100644 --- a/internal/service/appsync/datasource_test.go +++ b/internal/service/appsync/datasource_test.go @@ -36,6 +36,7 @@ func testAccAppSyncDataSource_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "elasticsearch_config.#", "0"), resource.TestCheckResourceAttr(resourceName, "http_config.#", "0"), resource.TestCheckResourceAttr(resourceName, "lambda_config.#", "0"), + resource.TestCheckResourceAttr(resourceName, "relational_database_config.#", "0"), resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "type", "NONE"), ), @@ -345,6 +346,97 @@ func testAccAppSyncDataSource_Type_http(t *testing.T) { }) } +func testAccAppSyncDataSource_Type_http_auth(t *testing.T) { + rName := fmt.Sprintf("tfacctest%d", sdkacctest.RandInt()) + resourceName := "aws_appsync_datasource.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(appsync.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, appsync.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDestroyDataSource, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncDatasourceConfig_Type_HTTPAuth(rName, acctest.Region()), + Check: resource.ComposeTestCheckFunc( + testAccCheckExistsDataSource(resourceName), + resource.TestCheckResourceAttr(resourceName, "http_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "http_config.0.endpoint", fmt.Sprintf("https://appsync.%s.amazonaws.com/", acctest.Region())), + resource.TestCheckResourceAttr(resourceName, "http_config.0.authorization_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "http_config.0.authorization_config.0.authorization_type", "AWS_IAM"), + resource.TestCheckResourceAttr(resourceName, "http_config.0.authorization_config.0.aws_iam_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "http_config.0.authorization_config.0.aws_iam_config.0.signing_region", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "http_config.0.authorization_config.0.aws_iam_config.0.signing_service_name", "appsync"), + resource.TestCheckResourceAttr(resourceName, "type", "HTTP"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAppsyncDatasource_Type_RelationalDatabase(t *testing.T) { + rName := fmt.Sprintf("tfacctest%d", sdkacctest.RandInt()) + resourceName := "aws_appsync_datasource.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(appsync.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, appsync.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDestroyDataSource, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncDatasourceConfigTypeRelationalDatabase(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExistsDataSource(resourceName), + resource.TestCheckResourceAttr(resourceName, "relational_database_config.0.http_endpoint_config.0.region", acctest.Region()), + resource.TestCheckResourceAttrPair(resourceName, "relational_database_config.0.http_endpoint_config.0.db_cluster_identifier", "aws_rds_cluster.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "relational_database_config.0.http_endpoint_config.0.aws_secret_store_arn", "aws_secretsmanager_secret.test", "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAppsyncDatasource_Type_RelationalDatabaseWithOptions(t *testing.T) { + rName := fmt.Sprintf("tfacctest%d", sdkacctest.RandInt()) + resourceName := "aws_appsync_datasource.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(appsync.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, appsync.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDestroyDataSource, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncDatasourceConfigTypeRelationalDatabaseWithOptions(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExistsDataSource(resourceName), + resource.TestCheckResourceAttr(resourceName, "relational_database_config.0.http_endpoint_config.0.schema", "mydb"), + resource.TestCheckResourceAttr(resourceName, "relational_database_config.0.http_endpoint_config.0.region", acctest.Region()), + resource.TestCheckResourceAttrPair(resourceName, "relational_database_config.0.http_endpoint_config.0.db_cluster_identifier", "aws_rds_cluster.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "relational_database_config.0.http_endpoint_config.0.database_name", "aws_rds_cluster.test", "database_name"), + resource.TestCheckResourceAttrPair(resourceName, "relational_database_config.0.http_endpoint_config.0.aws_secret_store_arn", "aws_secretsmanager_secret.test", "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccAppSyncDataSource_Type_lambda(t *testing.T) { rName := fmt.Sprintf("tfacctest%d", sdkacctest.RandInt()) iamRoleResourceName := "aws_iam_role.test" @@ -786,19 +878,192 @@ func testAccAppsyncDatasourceConfig_Type_HTTP(rName string) string { return fmt.Sprintf(` resource "aws_appsync_graphql_api" "test" { authentication_type = "API_KEY" - name = %q + name = %[1]q } resource "aws_appsync_datasource" "test" { api_id = aws_appsync_graphql_api.test.id - name = %q + name = %[1]q type = "HTTP" http_config { endpoint = "http://example.com" } } -`, rName, rName) +`, rName) +} + +func testAccAppsyncDatasourceConfig_Type_HTTPAuth(rName, region string) string { + return fmt.Sprintf(` +resource "aws_appsync_graphql_api" "test" { + authentication_type = "API_KEY" + name = %[1]q +} + +resource "aws_iam_role" "test" { + name = %[1]q + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : "sts:AssumeRole", + "Principal" : { + "Service" : "appsync.amazonaws.com" + }, + "Effect" : "Allow" + } + ] + }) +} + +resource "aws_iam_role_policy" "test" { + role = aws_iam_role.test.id + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : [ + "appsync:ListGraphqlApis" + ], + "Effect" : "Allow", + "Resource" : [ + "*" + ] + } + ] + }) +} + +resource "aws_appsync_datasource" "test" { + api_id = aws_appsync_graphql_api.test.id + name = %[1]q + type = "HTTP" + service_role_arn = aws_iam_role.test.arn + + http_config { + endpoint = "https://appsync.%[2]s.amazonaws.com/" + + authorization_config { + authorization_type = "AWS_IAM" + + aws_iam_config { + signing_region = %[2]q + signing_service_name = "appsync" + } + } + } +} +`, rName, region) +} + +func testAccAppsyncDatasourceConfigBaseRelationalDatabase(rName string) string { + return fmt.Sprintf(` +resource "aws_secretsmanager_secret" "test" { + name = %[1]q +} + +resource "aws_secretsmanager_secret_version" "test" { + secret_id = aws_secretsmanager_secret.test.id + secret_string = jsonencode( + { + username = "foo" + password = "mustbeeightcharaters" + } + ) +} + +resource "aws_rds_cluster" "test" { + cluster_identifier = %[1]q + engine_mode = "serverless" + database_name = "mydb" + master_username = "foo" + master_password = "mustbeeightcharaters" + db_cluster_parameter_group_name = "default.aurora5.6" + skip_final_snapshot = true + + scaling_configuration { + min_capacity = 1 + max_capacity = 2 + } +} + +resource "aws_iam_role" "test" { + name = %[1]q + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : "sts:AssumeRole", + "Principal" : { + "Service" : "appsync.amazonaws.com" + }, + "Effect" : "Allow" + } + ] + }) +} + +resource "aws_iam_role_policy" "test" { + role = aws_iam_role.test.id + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : [ + "rds:*" + ], + "Effect" : "Allow", + "Resource" : [ + aws_rds_cluster.test.arn + ] + } + ] + }) +} + +resource "aws_appsync_graphql_api" "test" { + authentication_type = "API_KEY" + name = %[1]q +} +`, rName) +} + +func testAccAppsyncDatasourceConfigTypeRelationalDatabase(rName string) string { + return testAccAppsyncDatasourceConfigBaseRelationalDatabase(rName) + fmt.Sprintf(` +resource "aws_appsync_datasource" "test" { + api_id = aws_appsync_graphql_api.test.id + name = %[1]q + service_role_arn = aws_iam_role.test.arn + type = "RELATIONAL_DATABASE" + + relational_database_config { + http_endpoint_config { + db_cluster_identifier = aws_rds_cluster.test.id + aws_secret_store_arn = aws_secretsmanager_secret.test.arn + } + } +} +`, rName) +} + +func testAccAppsyncDatasourceConfigTypeRelationalDatabaseWithOptions(rName string) string { + return testAccAppsyncDatasourceConfigBaseRelationalDatabase(rName) + fmt.Sprintf(` +resource "aws_appsync_datasource" "test" { + api_id = aws_appsync_graphql_api.test.id + name = %[1]q + service_role_arn = aws_iam_role.test.arn + type = "RELATIONAL_DATABASE" + + relational_database_config { + http_endpoint_config { + db_cluster_identifier = aws_rds_cluster.test.id + database_name = aws_rds_cluster.test.database_name + aws_secret_store_arn = aws_secretsmanager_secret.test.arn + schema = "mydb" + } + } +} +`, rName) } func testAccAppsyncDatasourceConfig_Type_Lambda(rName string) string { diff --git a/website/docs/r/appsync_datasource.html.markdown b/website/docs/r/appsync_datasource.html.markdown index be01c499237..5b4bce931a1 100644 --- a/website/docs/r/appsync_datasource.html.markdown +++ b/website/docs/r/appsync_datasource.html.markdown @@ -89,13 +89,14 @@ The following arguments are supported: * `api_id` - (Required) The API ID for the GraphQL API for the DataSource. * `name` - (Required) A user-supplied name for the DataSource. -* `type` - (Required) The type of the DataSource. Valid values: `AWS_LAMBDA`, `AMAZON_DYNAMODB`, `AMAZON_ELASTICSEARCH`, `HTTP`, `NONE`. +* `type` - (Required) The type of the DataSource. Valid values: `AWS_LAMBDA`, `AMAZON_DYNAMODB`, `AMAZON_ELASTICSEARCH`, `HTTP`, `NONE`, `RELATIONAL_DATABASE`. * `description` - (Optional) A description of the DataSource. * `service_role_arn` - (Optional) The IAM service role ARN for the data source. * `dynamodb_config` - (Optional) DynamoDB settings. See [below](#dynamodb_config) * `elasticsearch_config` - (Optional) Amazon Elasticsearch settings. See [below](#elasticsearch_config) * `http_config` - (Optional) HTTP settings. See [below](#http_config) * `lambda_config` - (Optional) AWS Lambda settings. See [below](#lambda_config) +* `relational_database_config` (Optional) AWS RDS settings. See [Relational Database Config](#relational_database_config) ### dynamodb_config @@ -117,6 +118,38 @@ The following arguments are supported: The following arguments are supported: * `endpoint` - (Required) HTTP URL. +* `authorization_config` - (Optional) The authorization configuration in case the HTTP endpoint requires authorization. See [Authorization Config](#authorization_config). + +#### authorization_config + +The following arguments are supported: + +* `authorization_type` - (Optional) The authorization type that the HTTP endpoint requires. Default values is `AWS_IAM`. +* `aws_iam_config` - (Optional) The Identity and Access Management (IAM) settings. See [AWS IAM Config](#aws_iam_config). + +##### aws_iam_config + +The following arguments are supported: + +* `signing_region` - (Optional) The signing Amazon Web Services Region for IAM authorization. +* `signing_service_name`- (Optional) The signing service name for IAM authorization. + +### relational_database_config + +The following arguments are supported: + +* `http_endpoint_config` - (Required) The Amazon RDS HTTP endpoint configuration. See [HTTP Endpoint Config](#http_endpoint_config). +* `source_type` - (Optional) Source type for the relational database. Valid values: `RDS_HTTP_ENDPOINT`. + +#### http_endpoint_config + +The following arguments are supported: + +* `db_cluster_identifier` - (Required) Amazon RDS cluster identifier. +* `aws_secret_store_arn` - (Required) AWS secret store ARN for database credentials. +* `database_name` - (Optional) Logical database name. +* `region` - (Optional) AWS Region for RDS HTTP endpoint. Defaults to current region. +* `schema` - (Optional) Logical schema name. ### lambda_config