diff --git a/.ci/.semgrep-service-name0.yml b/.ci/.semgrep-service-name0.yml index 9385dee12ae7..0113ceb4d335 100644 --- a/.ci/.semgrep-service-name0.yml +++ b/.ci/.semgrep-service-name0.yml @@ -4529,3 +4529,17 @@ rules: patterns: - pattern-regex: "(?i)ConfigService" severity: WARNING + - id: configservice-in-var-name + languages: + - go + message: Do not use "ConfigService" in var name inside configservice package + paths: + include: + - "/internal/service/configservice" + patterns: + - pattern: var $NAME = ... + - metavariable-pattern: + metavariable: $NAME + patterns: + - pattern-regex: "(?i)ConfigService" + severity: WARNING diff --git a/.ci/.semgrep-service-name1.yml b/.ci/.semgrep-service-name1.yml index e8cff4304769..e6b974ed1a93 100644 --- a/.ci/.semgrep-service-name1.yml +++ b/.ci/.semgrep-service-name1.yml @@ -3,20 +3,6 @@ # Generated by internal/generate/servicesemgrep/main.go; DO NOT EDIT. rules: - - id: configservice-in-var-name - languages: - - go - message: Do not use "ConfigService" in var name inside configservice package - paths: - include: - - "/internal/service/configservice" - patterns: - - pattern: var $NAME = ... - - metavariable-pattern: - metavariable: $NAME - patterns: - - pattern-regex: "(?i)ConfigService" - severity: WARNING - id: connect-in-func-name languages: - go @@ -4525,3 +4511,31 @@ rules: - pattern-not-regex: "^TestAccIVS" - pattern-regex: ^TestAcc.* severity: WARNING + - id: ivs-in-const-name + languages: + - go + message: Do not use "IVS" in const name inside ivs package + paths: + include: + - "/internal/service/ivs" + patterns: + - pattern: const $NAME = ... + - metavariable-pattern: + metavariable: $NAME + patterns: + - pattern-regex: "(?i)IVS" + severity: WARNING + - id: ivs-in-var-name + languages: + - go + message: Do not use "IVS" in var name inside ivs package + paths: + include: + - "/internal/service/ivs" + patterns: + - pattern: var $NAME = ... + - metavariable-pattern: + metavariable: $NAME + patterns: + - pattern-regex: "(?i)IVS" + severity: WARNING diff --git a/.ci/.semgrep-service-name2.yml b/.ci/.semgrep-service-name2.yml index 90d6960e7e3e..f0aa6d4ae89c 100644 --- a/.ci/.semgrep-service-name2.yml +++ b/.ci/.semgrep-service-name2.yml @@ -3,34 +3,6 @@ # Generated by internal/generate/servicesemgrep/main.go; DO NOT EDIT. rules: - - id: ivs-in-const-name - languages: - - go - message: Do not use "IVS" in const name inside ivs package - paths: - include: - - "/internal/service/ivs" - patterns: - - pattern: const $NAME = ... - - metavariable-pattern: - metavariable: $NAME - patterns: - - pattern-regex: "(?i)IVS" - severity: WARNING - - id: ivs-in-var-name - languages: - - go - message: Do not use "IVS" in var name inside ivs package - paths: - include: - - "/internal/service/ivs" - patterns: - - pattern: var $NAME = ... - - metavariable-pattern: - metavariable: $NAME - patterns: - - pattern-regex: "(?i)IVS" - severity: WARNING - id: ivschat-in-func-name languages: - go @@ -2198,6 +2170,67 @@ rules: patterns: - pattern-regex: "(?i)Mgn" severity: WARNING + - id: mpa-in-func-name + languages: + - go + message: Do not use "MPA" in func name inside mpa package + paths: + include: + - "/internal/service/mpa" + exclude: + - "/internal/service/mpa/list_pages_gen.go" + patterns: + - pattern: func $NAME( ... ) + - metavariable-pattern: + metavariable: $NAME + patterns: + - pattern-regex: "(?i)MPA" + - focus-metavariable: $NAME + - pattern-not: func $NAME($T *testing.T) + severity: WARNING + - id: mpa-in-test-name + languages: + - go + message: Include "MPA" in test name + paths: + include: + - "/internal/service/mpa/*_test.go" + patterns: + - pattern: func $NAME( ... ) + - metavariable-pattern: + metavariable: $NAME + patterns: + - pattern-not-regex: "^TestAccMPA" + - pattern-regex: ^TestAcc.* + severity: WARNING + - id: mpa-in-const-name + languages: + - go + message: Do not use "MPA" in const name inside mpa package + paths: + include: + - "/internal/service/mpa" + patterns: + - pattern: const $NAME = ... + - metavariable-pattern: + metavariable: $NAME + patterns: + - pattern-regex: "(?i)MPA" + severity: WARNING + - id: mpa-in-var-name + languages: + - go + message: Do not use "MPA" in var name inside mpa package + paths: + include: + - "/internal/service/mpa" + patterns: + - pattern: var $NAME = ... + - metavariable-pattern: + metavariable: $NAME + patterns: + - pattern-regex: "(?i)MPA" + severity: WARNING - id: mq-in-func-name languages: - go @@ -4508,21 +4541,3 @@ rules: patterns: - pattern-regex: "(?i)RDSData" severity: WARNING - - id: rdsdataservice-in-func-name - languages: - - go - message: Do not use "rdsdataservice" in func name inside rdsdata package - paths: - include: - - "/internal/service/rdsdata" - exclude: - - "/internal/service/rdsdata/list_pages_gen.go" - patterns: - - pattern: func $NAME( ... ) - - metavariable-pattern: - metavariable: $NAME - patterns: - - pattern-regex: "(?i)rdsdataservice" - - focus-metavariable: $NAME - - pattern-not: func $NAME($T *testing.T) - severity: WARNING diff --git a/.ci/.semgrep-service-name3.yml b/.ci/.semgrep-service-name3.yml index f2f2648df81d..99fa072f99f2 100644 --- a/.ci/.semgrep-service-name3.yml +++ b/.ci/.semgrep-service-name3.yml @@ -3,6 +3,24 @@ # Generated by internal/generate/servicesemgrep/main.go; DO NOT EDIT. rules: + - id: rdsdataservice-in-func-name + languages: + - go + message: Do not use "rdsdataservice" in func name inside rdsdata package + paths: + include: + - "/internal/service/rdsdata" + exclude: + - "/internal/service/rdsdata/list_pages_gen.go" + patterns: + - pattern: func $NAME( ... ) + - metavariable-pattern: + metavariable: $NAME + patterns: + - pattern-regex: "(?i)rdsdataservice" + - focus-metavariable: $NAME + - pattern-not: func $NAME($T *testing.T) + severity: WARNING - id: rdsdataservice-in-const-name languages: - go diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index 7bcfffb61082..c73b1ccc6150 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -490,6 +490,8 @@ service/migrationhubstrategy: - '((\*|-)\s*`?|(data|resource)\s+"?)aws_migrationhubstrategy_' service/mobile: - '((\*|-)\s*`?|(data|resource)\s+"?)aws_mobile_' +service/mpa: + - '((\*|-)\s*`?|(data|resource)\s+"?)aws_mpa_' service/mq: - '((\*|-)\s*`?|(data|resource)\s+"?)aws_mq_' service/mturk: diff --git a/.github/labeler-pr-triage.yml b/.github/labeler-pr-triage.yml index aa35b0b9335c..5692316f9df0 100644 --- a/.github/labeler-pr-triage.yml +++ b/.github/labeler-pr-triage.yml @@ -1553,6 +1553,12 @@ service/mobile: - any-glob-to-any-file: - 'internal/service/mobile/**/*' - 'website/**/mobile_*' +service/mpa: + - any: + - changed-files: + - any-glob-to-any-file: + - 'internal/service/mpa/**/*' + - 'website/**/mpa_*' service/mq: - any: - changed-files: diff --git a/.teamcity/components/generated/services_all.kt b/.teamcity/components/generated/services_all.kt index 982bff32e63d..5ea14def76f7 100644 --- a/.teamcity/components/generated/services_all.kt +++ b/.teamcity/components/generated/services_all.kt @@ -158,6 +158,7 @@ val services = mapOf( "mediastore" to ServiceSpec("Elemental MediaStore"), "memorydb" to ServiceSpec("MemoryDB"), "mgn" to ServiceSpec("Application Migration (Mgn)"), + "mpa" to ServiceSpec("Multi-party Approval"), "mq" to ServiceSpec("MQ", vpcLock = true), "mwaa" to ServiceSpec("MWAA (Managed Workflows for Apache Airflow)", vpcLock = true), "mwaaserverless" to ServiceSpec("MWAA (Managed Workflows for Apache Airflow) Serverless"), diff --git a/go.mod b/go.mod index db6abf024910..2403798ba6f1 100644 --- a/go.mod +++ b/go.mod @@ -175,6 +175,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/mediastore v1.29.16 github.com/aws/aws-sdk-go-v2/service/memorydb v1.33.9 github.com/aws/aws-sdk-go-v2/service/mgn v1.39.0 + github.com/aws/aws-sdk-go-v2/service/mpa v1.5.8 github.com/aws/aws-sdk-go-v2/service/mq v1.34.14 github.com/aws/aws-sdk-go-v2/service/mwaa v1.39.16 github.com/aws/aws-sdk-go-v2/service/mwaaserverless v1.0.4 diff --git a/go.sum b/go.sum index b02fab9c05e6..842b20852df4 100644 --- a/go.sum +++ b/go.sum @@ -371,6 +371,8 @@ github.com/aws/aws-sdk-go-v2/service/memorydb v1.33.9 h1:ctix7klitYypTyR16+PriNV github.com/aws/aws-sdk-go-v2/service/memorydb v1.33.9/go.mod h1:HyiJ5TWJG3fhWLJ4GIuduUVEEXvkO2jh6ojuuuNiXBE= github.com/aws/aws-sdk-go-v2/service/mgn v1.39.0 h1:fNdWXDvomprT+2NGXpAeDRgeOtZugpRSZBRaBUzHtK4= github.com/aws/aws-sdk-go-v2/service/mgn v1.39.0/go.mod h1:O7jBg7J0JaKfEYIwsiUVdZbpYL6/BGAjhWTlSfbKbJ4= +github.com/aws/aws-sdk-go-v2/service/mpa v1.5.8 h1:QKS9c9fZvj25JC2mD+X2U2n+h9n1aOxv+WFkZFThVuo= +github.com/aws/aws-sdk-go-v2/service/mpa v1.5.8/go.mod h1:sU6pVs5NzPa9BFG5/z/Y4zI2lFQxr9sbbJS0B9HtYcc= github.com/aws/aws-sdk-go-v2/service/mq v1.34.14 h1:mqy/sNV+hiOfa5/GVyAD3Ru25M9ZRmnJrupwTbnbjLA= github.com/aws/aws-sdk-go-v2/service/mq v1.34.14/go.mod h1:ux7Ft3OASBbfFRODkjpRBaskK6kDBquWWMRgadU9uWg= github.com/aws/aws-sdk-go-v2/service/mwaa v1.39.16 h1:hjz/MS88eIbATk6csCtbgGQzemvq989WTA0SR068GM0= diff --git a/infrastructure/repository/labels-service.tf b/infrastructure/repository/labels-service.tf index f9d49bb46e39..271b41705c62 100644 --- a/infrastructure/repository/labels-service.tf +++ b/infrastructure/repository/labels-service.tf @@ -234,6 +234,7 @@ variable "service_labels" { "migrationhubrefactorspaces", "migrationhubstrategy", "mobile", + "mpa", "mq", "mturk", "mwaa", diff --git a/internal/conns/awsclient_gen.go b/internal/conns/awsclient_gen.go index efdb0f848efd..95d167cb5a5c 100644 --- a/internal/conns/awsclient_gen.go +++ b/internal/conns/awsclient_gen.go @@ -166,6 +166,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/mediastore" "github.com/aws/aws-sdk-go-v2/service/memorydb" "github.com/aws/aws-sdk-go-v2/service/mgn" + "github.com/aws/aws-sdk-go-v2/service/mpa" "github.com/aws/aws-sdk-go-v2/service/mq" "github.com/aws/aws-sdk-go-v2/service/mwaa" "github.com/aws/aws-sdk-go-v2/service/mwaaserverless" @@ -866,6 +867,10 @@ func (c *AWSClient) M2Client(ctx context.Context) *m2.Client { return errs.Must(client[*m2.Client](ctx, c, names.M2, make(map[string]any))) } +func (c *AWSClient) MPAClient(ctx context.Context) *mpa.Client { + return errs.Must(client[*mpa.Client](ctx, c, names.MPA, make(map[string]any))) +} + func (c *AWSClient) MQClient(ctx context.Context) *mq.Client { return errs.Must(client[*mq.Client](ctx, c, names.MQ, make(map[string]any))) } diff --git a/internal/provider/framework/provider_gen.go b/internal/provider/framework/provider_gen.go index 23b532a22dc1..8229151192a2 100644 --- a/internal/provider/framework/provider_gen.go +++ b/internal/provider/framework/provider_gen.go @@ -1296,6 +1296,13 @@ func endpointsBlock() schema.SetNestedBlock { Description: "Use this to override the default service endpoint URL", }, + // mpa + + "mpa": schema.StringAttribute{ + Optional: true, + Description: "Use this to override the default service endpoint URL", + }, + // mq "mq": schema.StringAttribute{ diff --git a/internal/provider/sdkv2/provider_gen.go b/internal/provider/sdkv2/provider_gen.go index 26b588fc5a9d..28f393c81f68 100644 --- a/internal/provider/sdkv2/provider_gen.go +++ b/internal/provider/sdkv2/provider_gen.go @@ -1498,6 +1498,14 @@ func endpointsSchema() *schema.Schema { Description: "Use this to override the default service endpoint URL", }, + // mpa + + "mpa": { + Type: schema.TypeString, + Optional: true, + Description: "Use this to override the default service endpoint URL", + }, + // mq "mq": { diff --git a/internal/provider/sdkv2/service_packages_gen.go b/internal/provider/sdkv2/service_packages_gen.go index 34952c466075..0b2281994706 100644 --- a/internal/provider/sdkv2/service_packages_gen.go +++ b/internal/provider/sdkv2/service_packages_gen.go @@ -170,6 +170,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/service/memorydb" "github.com/hashicorp/terraform-provider-aws/internal/service/meta" "github.com/hashicorp/terraform-provider-aws/internal/service/mgn" + "github.com/hashicorp/terraform-provider-aws/internal/service/mpa" "github.com/hashicorp/terraform-provider-aws/internal/service/mq" "github.com/hashicorp/terraform-provider-aws/internal/service/mwaa" "github.com/hashicorp/terraform-provider-aws/internal/service/mwaaserverless" @@ -434,6 +435,7 @@ func servicePackages(ctx context.Context) []conns.ServicePackage { memorydb.ServicePackage(ctx), meta.ServicePackage(ctx), mgn.ServicePackage(ctx), + mpa.ServicePackage(ctx), mq.ServicePackage(ctx), mwaa.ServicePackage(ctx), mwaaserverless.ServicePackage(ctx), diff --git a/internal/service/mpa/generate.go b/internal/service/mpa/generate.go new file mode 100644 index 000000000000..a34f7b4d96fc --- /dev/null +++ b/internal/service/mpa/generate.go @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +//go:generate go run ../../generate/servicepackage/main.go +// ONLY generate directives and package declaration! Do not add anything else to this file. + +package mpa diff --git a/internal/service/mpa/service_endpoint_resolver_gen.go b/internal/service/mpa/service_endpoint_resolver_gen.go new file mode 100644 index 000000000000..e609245f8f23 --- /dev/null +++ b/internal/service/mpa/service_endpoint_resolver_gen.go @@ -0,0 +1,86 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/servicepackage/main.go; DO NOT EDIT. + +package mpa + +import ( + "context" + "fmt" + "net" + + "github.com/YakDriver/smarterr" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/mpa" + smithyendpoints "github.com/aws/smithy-go/endpoints" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/errs" +) + +var _ mpa.EndpointResolverV2 = resolverV2{} + +type resolverV2 struct { + defaultResolver mpa.EndpointResolverV2 +} + +func newEndpointResolverV2() resolverV2 { + return resolverV2{ + defaultResolver: mpa.NewDefaultEndpointResolverV2(), + } +} + +func (r resolverV2) ResolveEndpoint(ctx context.Context, params mpa.EndpointParameters) (endpoint smithyendpoints.Endpoint, err error) { + params = params.WithDefaults() + useFIPS := aws.ToBool(params.UseFIPS) + + if eps := params.Endpoint; aws.ToString(eps) != "" { + tflog.Debug(ctx, "setting endpoint", map[string]any{ + "tf_aws.endpoint": endpoint, + }) + + if useFIPS { + tflog.Debug(ctx, "endpoint set, ignoring UseFIPSEndpoint setting") + params.UseFIPS = aws.Bool(false) + } + + return r.defaultResolver.ResolveEndpoint(ctx, params) + } else if useFIPS { + ctx = tflog.SetField(ctx, "tf_aws.use_fips", useFIPS) + + endpoint, err = r.defaultResolver.ResolveEndpoint(ctx, params) + if err != nil { + return endpoint, smarterr.NewError(err) + } + + tflog.Debug(ctx, "endpoint resolved", map[string]any{ + "tf_aws.endpoint": endpoint.URI.String(), + }) + + hostname := endpoint.URI.Hostname() + _, err = net.LookupHost(hostname) + if err != nil { + if dnsErr, ok := errs.As[*net.DNSError](err); ok && dnsErr.IsNotFound { + tflog.Debug(ctx, "default endpoint host not found, disabling FIPS", map[string]any{ + "tf_aws.hostname": hostname, + }) + params.UseFIPS = aws.Bool(false) + } else { + err = fmt.Errorf("looking up mpa endpoint %q: %w", hostname, err) + return + } + } else { + return endpoint, smarterr.NewError(err) + } + } + + return r.defaultResolver.ResolveEndpoint(ctx, params) +} + +func withBaseEndpoint(endpoint string) func(*mpa.Options) { + return func(o *mpa.Options) { + if endpoint != "" { + o.BaseEndpoint = aws.String(endpoint) + } + } +} diff --git a/internal/service/mpa/service_endpoints_gen_test.go b/internal/service/mpa/service_endpoints_gen_test.go new file mode 100644 index 000000000000..7644e720d950 --- /dev/null +++ b/internal/service/mpa/service_endpoints_gen_test.go @@ -0,0 +1,605 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/serviceendpointtests/main.go; DO NOT EDIT. + +package mpa_test + +import ( + "context" + "errors" + "fmt" + "maps" + "net" + "net/url" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/aws-sdk-go-v2/service/mpa" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/aws-sdk-go-base/v2/servicemocks" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + terraformsdk "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/provider/sdkv2" + "github.com/hashicorp/terraform-provider-aws/names" +) + +type endpointTestCase struct { + with []setupFunc + expected caseExpectations +} + +type caseSetup struct { + config map[string]any + configFile configFile + environmentVariables map[string]string +} + +type configFile struct { + baseUrl string + serviceUrl string +} + +type caseExpectations struct { + diags diag.Diagnostics + endpoint string + region string +} + +type apiCallParams struct { + endpoint string + region string +} + +type setupFunc func(setup *caseSetup) + +type callFunc func(ctx context.Context, t *testing.T, meta *conns.AWSClient) apiCallParams + +const ( + packageNameConfigEndpoint = "https://packagename-config.endpoint.test/" + awsServiceEnvvarEndpoint = "https://service-envvar.endpoint.test/" + baseEnvvarEndpoint = "https://base-envvar.endpoint.test/" + serviceConfigFileEndpoint = "https://service-configfile.endpoint.test/" + baseConfigFileEndpoint = "https://base-configfile.endpoint.test/" +) + +const ( + packageName = "mpa" + awsEnvVar = "AWS_ENDPOINT_URL_MPA" + baseEnvVar = "AWS_ENDPOINT_URL" + configParam = "mpa" +) + +const ( + expectedCallRegion = "us-east-1" //lintignore:AWSAT003 +) + +func TestEndpointConfiguration(t *testing.T) { //nolint:paralleltest // uses t.Setenv + ctx := t.Context() + const providerRegion = "us-west-2" //lintignore:AWSAT003 + const expectedEndpointRegion = providerRegion + + testcases := map[string]endpointTestCase{ + "no config": { + with: []setupFunc{withNoConfig}, + expected: expectDefaultEndpoint(ctx, t, expectedEndpointRegion), + }, + + // Package name endpoint on Config + + "package name endpoint config": { + with: []setupFunc{ + withPackageNameEndpointInConfig, + }, + expected: expectPackageNameConfigEndpoint(), + }, + + "package name endpoint config overrides aws service envvar": { + with: []setupFunc{ + withPackageNameEndpointInConfig, + withAwsEnvVar, + }, + expected: expectPackageNameConfigEndpoint(), + }, + + "package name endpoint config overrides base envvar": { + with: []setupFunc{ + withPackageNameEndpointInConfig, + withBaseEnvVar, + }, + expected: expectPackageNameConfigEndpoint(), + }, + + "package name endpoint config overrides service config file": { + with: []setupFunc{ + withPackageNameEndpointInConfig, + withServiceEndpointInConfigFile, + }, + expected: expectPackageNameConfigEndpoint(), + }, + + "package name endpoint config overrides base config file": { + with: []setupFunc{ + withPackageNameEndpointInConfig, + withBaseEndpointInConfigFile, + }, + expected: expectPackageNameConfigEndpoint(), + }, + + // Service endpoint in AWS envvar + + "service aws envvar": { + with: []setupFunc{ + withAwsEnvVar, + }, + expected: expectAwsEnvVarEndpoint(), + }, + + "service aws envvar overrides base envvar": { + with: []setupFunc{ + withAwsEnvVar, + withBaseEnvVar, + }, + expected: expectAwsEnvVarEndpoint(), + }, + + "service aws envvar overrides service config file": { + with: []setupFunc{ + withAwsEnvVar, + withServiceEndpointInConfigFile, + }, + expected: expectAwsEnvVarEndpoint(), + }, + + "service aws envvar overrides base config file": { + with: []setupFunc{ + withAwsEnvVar, + withBaseEndpointInConfigFile, + }, + expected: expectAwsEnvVarEndpoint(), + }, + + // Base endpoint in envvar + + "base endpoint envvar": { + with: []setupFunc{ + withBaseEnvVar, + }, + expected: expectBaseEnvVarEndpoint(), + }, + + "base endpoint envvar overrides service config file": { + with: []setupFunc{ + withBaseEnvVar, + withServiceEndpointInConfigFile, + }, + expected: expectBaseEnvVarEndpoint(), + }, + + "base endpoint envvar overrides base config file": { + with: []setupFunc{ + withBaseEnvVar, + withBaseEndpointInConfigFile, + }, + expected: expectBaseEnvVarEndpoint(), + }, + + // Service endpoint in config file + + "service config file": { + with: []setupFunc{ + withServiceEndpointInConfigFile, + }, + expected: expectServiceConfigFileEndpoint(), + }, + + "service config file overrides base config file": { + with: []setupFunc{ + withServiceEndpointInConfigFile, + withBaseEndpointInConfigFile, + }, + expected: expectServiceConfigFileEndpoint(), + }, + + // Base endpoint in config file + + "base endpoint config file": { + with: []setupFunc{ + withBaseEndpointInConfigFile, + }, + expected: expectBaseConfigFileEndpoint(), + }, + + // Use FIPS endpoint on Config + + "use fips config": { + with: []setupFunc{ + withUseFIPSInConfig, + }, + expected: expectDefaultFIPSEndpoint(ctx, t, expectedEndpointRegion), + }, + + "use fips config with package name endpoint config": { + with: []setupFunc{ + withUseFIPSInConfig, + withPackageNameEndpointInConfig, + }, + expected: expectPackageNameConfigEndpoint(), + }, + } + + for name, testcase := range testcases { //nolint:paralleltest // uses t.Setenv + t.Run(name, func(t *testing.T) { + testEndpointCase(ctx, t, providerRegion, testcase, callService) + }) + } +} + +func defaultEndpoint(ctx context.Context, region string) (url.URL, error) { + r := mpa.NewDefaultEndpointResolverV2() + + ep, err := r.ResolveEndpoint(ctx, mpa.EndpointParameters{ + Region: aws.String(region), + }) + if err != nil { + return url.URL{}, err + } + + if ep.URI.Path == "" { + ep.URI.Path = "/" + } + + return ep.URI, nil +} + +func defaultFIPSEndpoint(ctx context.Context, region string) (url.URL, error) { + r := mpa.NewDefaultEndpointResolverV2() + + ep, err := r.ResolveEndpoint(ctx, mpa.EndpointParameters{ + Region: aws.String(region), + UseFIPS: aws.Bool(true), + }) + if err != nil { + return url.URL{}, err + } + + if ep.URI.Path == "" { + ep.URI.Path = "/" + } + + return ep.URI, nil +} + +func callService(ctx context.Context, t *testing.T, meta *conns.AWSClient) apiCallParams { + t.Helper() + + client := meta.MPAClient(ctx) + + var result apiCallParams + + input := mpa.ListApprovalTeamsInput{} + _, err := client.ListApprovalTeams(ctx, &input, + func(opts *mpa.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &result.endpoint), + addRetrieveRegionMiddleware(&result.region), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Unexpected error: %s", err) + } + + return result +} + +func withNoConfig(_ *caseSetup) { + // no-op +} + +func withPackageNameEndpointInConfig(setup *caseSetup) { + if _, ok := setup.config[names.AttrEndpoints]; !ok { + setup.config[names.AttrEndpoints] = []any{ + map[string]any{}, + } + } + endpoints := setup.config[names.AttrEndpoints].([]any)[0].(map[string]any) + endpoints[packageName] = packageNameConfigEndpoint +} + +func withAwsEnvVar(setup *caseSetup) { + setup.environmentVariables[awsEnvVar] = awsServiceEnvvarEndpoint +} + +func withBaseEnvVar(setup *caseSetup) { + setup.environmentVariables[baseEnvVar] = baseEnvvarEndpoint +} + +func withServiceEndpointInConfigFile(setup *caseSetup) { + setup.configFile.serviceUrl = serviceConfigFileEndpoint +} + +func withBaseEndpointInConfigFile(setup *caseSetup) { + setup.configFile.baseUrl = baseConfigFileEndpoint +} + +func withUseFIPSInConfig(setup *caseSetup) { + setup.config["use_fips_endpoint"] = true +} + +func expectDefaultEndpoint(ctx context.Context, t *testing.T, region string) caseExpectations { + t.Helper() + + endpoint, err := defaultEndpoint(ctx, region) + if err != nil { + t.Fatalf("resolving accessanalyzer default endpoint: %s", err) + } + + return caseExpectations{ + endpoint: endpoint.String(), + region: expectedCallRegion, + } +} + +func expectDefaultFIPSEndpoint(ctx context.Context, t *testing.T, region string) caseExpectations { + t.Helper() + + endpoint, err := defaultFIPSEndpoint(ctx, region) + if err != nil { + t.Fatalf("resolving accessanalyzer FIPS endpoint: %s", err) + } + + hostname := endpoint.Hostname() + _, err = net.LookupHost(hostname) + if dnsErr, ok := errs.As[*net.DNSError](err); ok && dnsErr.IsNotFound { + return expectDefaultEndpoint(ctx, t, region) + } else if err != nil { + t.Fatalf("looking up accessanalyzer endpoint %q: %s", hostname, err) + } + + return caseExpectations{ + endpoint: endpoint.String(), + region: expectedCallRegion, + } +} + +func expectPackageNameConfigEndpoint() caseExpectations { + return caseExpectations{ + endpoint: packageNameConfigEndpoint, + region: expectedCallRegion, + } +} + +func expectAwsEnvVarEndpoint() caseExpectations { + return caseExpectations{ + endpoint: awsServiceEnvvarEndpoint, + region: expectedCallRegion, + } +} + +func expectBaseEnvVarEndpoint() caseExpectations { + return caseExpectations{ + endpoint: baseEnvvarEndpoint, + region: expectedCallRegion, + } +} + +func expectServiceConfigFileEndpoint() caseExpectations { + return caseExpectations{ + endpoint: serviceConfigFileEndpoint, + region: expectedCallRegion, + } +} + +func expectBaseConfigFileEndpoint() caseExpectations { + return caseExpectations{ + endpoint: baseConfigFileEndpoint, + region: expectedCallRegion, + } +} + +func testEndpointCase(ctx context.Context, t *testing.T, region string, testcase endpointTestCase, callF callFunc) { + t.Helper() + + setup := caseSetup{ + config: map[string]any{}, + environmentVariables: map[string]string{}, + } + + for _, f := range testcase.with { + f(&setup) + } + + config := map[string]any{ + names.AttrAccessKey: servicemocks.MockStaticAccessKey, + names.AttrSecretKey: servicemocks.MockStaticSecretKey, + names.AttrRegion: region, + names.AttrSkipCredentialsValidation: true, + names.AttrSkipRequestingAccountID: true, + } + + maps.Copy(config, setup.config) + + if setup.configFile.baseUrl != "" || setup.configFile.serviceUrl != "" { + config[names.AttrProfile] = "default" + tempDir := t.TempDir() + writeSharedConfigFile(t, &config, tempDir, generateSharedConfigFile(setup.configFile)) + } + + for k, v := range setup.environmentVariables { + t.Setenv(k, v) + } + + p, err := sdkv2.NewProvider(ctx) + if err != nil { + t.Fatal(err) + } + + p.TerraformVersion = "1.0.0" + + expectedDiags := testcase.expected.diags + diags := p.Configure(ctx, terraformsdk.NewResourceConfigRaw(config)) + + if diff := cmp.Diff(diags, expectedDiags, cmp.Comparer(sdkdiag.Comparer)); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diags.HasError() { + return + } + + meta := p.Meta().(*conns.AWSClient) + + callParams := callF(ctx, t, meta) + + if e, a := testcase.expected.endpoint, callParams.endpoint; e != a { + t.Errorf("expected endpoint %q, got %q", e, a) + } + + if e, a := testcase.expected.region, callParams.region; e != a { + t.Errorf("expected region %q, got %q", e, a) + } +} + +func addRetrieveEndpointURLMiddleware(t *testing.T, endpoint *string) func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Finalize.Add( + retrieveEndpointURLMiddleware(t, endpoint), + middleware.After, + ) + } +} + +func retrieveEndpointURLMiddleware(t *testing.T, endpoint *string) middleware.FinalizeMiddleware { + return middleware.FinalizeMiddlewareFunc( + "Test: Retrieve Endpoint", + func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { + t.Helper() + + request, ok := in.Request.(*smithyhttp.Request) + if !ok { + t.Fatalf("Expected *github.com/aws/smithy-go/transport/http.Request, got %s", fullTypeName(in.Request)) + } + + url := request.URL + url.RawQuery = "" + url.Path = "/" + + *endpoint = url.String() + + return next.HandleFinalize(ctx, in) + }) +} + +func addRetrieveRegionMiddleware(region *string) func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Serialize.Add( + retrieveRegionMiddleware(region), + middleware.After, + ) + } +} + +func retrieveRegionMiddleware(region *string) middleware.SerializeMiddleware { + return middleware.SerializeMiddlewareFunc( + "Test: Retrieve Region", + func(ctx context.Context, in middleware.SerializeInput, next middleware.SerializeHandler) (middleware.SerializeOutput, middleware.Metadata, error) { + *region = awsmiddleware.GetRegion(ctx) + + return next.HandleSerialize(ctx, in) + }, + ) +} + +var errCancelOperation = errors.New("Test: Canceling request") + +func addCancelRequestMiddleware() func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Finalize.Add( + cancelRequestMiddleware(), + middleware.After, + ) + } +} + +// cancelRequestMiddleware creates a Smithy middleware that intercepts the request before sending and cancels it +func cancelRequestMiddleware() middleware.FinalizeMiddleware { + return middleware.FinalizeMiddlewareFunc( + "Test: Cancel Requests", + func(_ context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { + return middleware.FinalizeOutput{}, middleware.Metadata{}, errCancelOperation + }) +} + +func fullTypeName(i any) string { + return fullValueTypeName(reflect.ValueOf(i)) +} + +func fullValueTypeName(v reflect.Value) string { + if v.Kind() == reflect.Ptr { + return "*" + fullValueTypeName(reflect.Indirect(v)) + } + + requestType := v.Type() + return fmt.Sprintf("%s.%s", requestType.PkgPath(), requestType.Name()) +} + +func generateSharedConfigFile(config configFile) string { + var buf strings.Builder + + buf.WriteString(` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`) + if config.baseUrl != "" { + fmt.Fprintf(&buf, "endpoint_url = %s\n", config.baseUrl) + } + + if config.serviceUrl != "" { + fmt.Fprintf(&buf, ` +services = endpoint-test + +[services endpoint-test] +%[1]s = + endpoint_url = %[2]s +`, configParam, serviceConfigFileEndpoint) + } + + return buf.String() +} + +func writeSharedConfigFile(t *testing.T, config *map[string]any, tempDir, content string) string { + t.Helper() + + file, err := os.Create(filepath.Join(tempDir, "aws-sdk-go-base-shared-configuration-file")) + if err != nil { + t.Fatalf("creating shared configuration file: %s", err) + } + + _, err = file.WriteString(content) + if err != nil { + t.Fatalf(" writing shared configuration file: %s", err) + } + + if v, ok := (*config)[names.AttrSharedConfigFiles]; !ok { + (*config)[names.AttrSharedConfigFiles] = []any{file.Name()} + } else { + (*config)[names.AttrSharedConfigFiles] = append(v.([]any), file.Name()) + } + + return file.Name() +} diff --git a/internal/service/mpa/service_package_gen.go b/internal/service/mpa/service_package_gen.go new file mode 100644 index 000000000000..2732141d6a46 --- /dev/null +++ b/internal/service/mpa/service_package_gen.go @@ -0,0 +1,104 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/servicepackage/main.go; DO NOT EDIT. + +package mpa + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/mpa" + "github.com/hashicorp/aws-sdk-go-base/v2/endpoints" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + inttypes "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/internal/vcr" + "github.com/hashicorp/terraform-provider-aws/names" +) + +type servicePackage struct{} + +func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*inttypes.ServicePackageFrameworkDataSource { + return []*inttypes.ServicePackageFrameworkDataSource{} +} + +func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.ServicePackageFrameworkResource { + return []*inttypes.ServicePackageFrameworkResource{} +} + +func (p *servicePackage) SDKDataSources(ctx context.Context) []*inttypes.ServicePackageSDKDataSource { + return []*inttypes.ServicePackageSDKDataSource{} +} + +func (p *servicePackage) SDKResources(ctx context.Context) []*inttypes.ServicePackageSDKResource { + return []*inttypes.ServicePackageSDKResource{} +} + +func (p *servicePackage) ServicePackageName() string { + return names.MPA +} + +// NewClient returns a new AWS SDK for Go v2 client for this service package's AWS API. +func (p *servicePackage) NewClient(ctx context.Context, config map[string]any) (*mpa.Client, error) { + cfg := *(config["aws_sdkv2_config"].(*aws.Config)) + optFns := []func(*mpa.Options){ + mpa.WithEndpointResolverV2(newEndpointResolverV2()), + withBaseEndpoint(config[names.AttrEndpoint].(string)), + func(o *mpa.Options) { + if region := config[names.AttrRegion].(string); o.Region != region { + tflog.Info(ctx, "overriding provider-configured AWS API region", map[string]any{ + "service": p.ServicePackageName(), + "original_region": o.Region, + "override_region": region, + }) + o.Region = region + } + }, + func(o *mpa.Options) { + if inContext, ok := conns.FromContext(ctx); ok && inContext.VCREnabled() { + tflog.Info(ctx, "overriding retry behavior to immediately return VCR errors") + o.Retryer = conns.AddIsErrorRetryables(cfg.Retryer().(aws.RetryerV2), vcr.InteractionNotFoundRetryableFunc) + } + }, + func(o *mpa.Options) { + switch partition := config["partition"].(string); partition { + case endpoints.AwsPartitionID: + if region := endpoints.UsEast1RegionID; o.Region != region { + tflog.Info(ctx, "overriding effective AWS API region", map[string]any{ + "service": p.ServicePackageName(), + "original_region": o.Region, + "override_region": region, + }) + o.Region = region + } + } + }, + withExtraOptions(ctx, p, config), + } + + return mpa.NewFromConfig(cfg, optFns...), nil +} + +// withExtraOptions returns a functional option that allows this service package to specify extra API client options. +// This option is always called after any generated options. +func withExtraOptions(ctx context.Context, sp conns.ServicePackage, config map[string]any) func(*mpa.Options) { + if v, ok := sp.(interface { + withExtraOptions(context.Context, map[string]any) []func(*mpa.Options) + }); ok { + optFns := v.withExtraOptions(ctx, config) + + return func(o *mpa.Options) { + for _, optFn := range optFns { + optFn(o) + } + } + } + + return func(*mpa.Options) {} +} + +func ServicePackage(ctx context.Context) conns.ServicePackage { + return &servicePackage{} +} diff --git a/internal/sweep/service_packages_gen_test.go b/internal/sweep/service_packages_gen_test.go index f4d9ca50b22e..35697267d1a3 100644 --- a/internal/sweep/service_packages_gen_test.go +++ b/internal/sweep/service_packages_gen_test.go @@ -170,6 +170,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/service/memorydb" "github.com/hashicorp/terraform-provider-aws/internal/service/meta" "github.com/hashicorp/terraform-provider-aws/internal/service/mgn" + "github.com/hashicorp/terraform-provider-aws/internal/service/mpa" "github.com/hashicorp/terraform-provider-aws/internal/service/mq" "github.com/hashicorp/terraform-provider-aws/internal/service/mwaa" "github.com/hashicorp/terraform-provider-aws/internal/service/mwaaserverless" @@ -434,6 +435,7 @@ func servicePackages(ctx context.Context) []conns.ServicePackage { memorydb.ServicePackage(ctx), meta.ServicePackage(ctx), mgn.ServicePackage(ctx), + mpa.ServicePackage(ctx), mq.ServicePackage(ctx), mwaa.ServicePackage(ctx), mwaaserverless.ServicePackage(ctx), diff --git a/names/consts_gen.go b/names/consts_gen.go index bde50eb5b521..c852a1c32442 100644 --- a/names/consts_gen.go +++ b/names/consts_gen.go @@ -154,6 +154,7 @@ const ( Location = "location" Logs = "logs" M2 = "m2" + MPA = "mpa" MQ = "mq" MWAA = "mwaa" MWAAServerless = "mwaaserverless" @@ -418,6 +419,7 @@ const ( LocationServiceID = "Location" LogsServiceID = "CloudWatch Logs" M2ServiceID = "m2" + MPAServiceID = "MPA" MQServiceID = "mq" MWAAServiceID = "MWAA" MWAAServerlessServiceID = "MWAA Serverless" diff --git a/names/data/names_data.hcl b/names/data/names_data.hcl index b1ae081b6708..9758353c9945 100644 --- a/names/data/names_data.hcl +++ b/names/data/names_data.hcl @@ -5822,6 +5822,33 @@ service "mobile" { not_implemented = true } +service "mpa" { + sdk { + id = "MPA" + arn_namespace = "mpa" + } + + names { + provider_name_upper = "MPA" + human_friendly = "Multi-party Approval" + } + + endpoint_info { + endpoint_api_call = "ListApprovalTeams" + endpoint_region_overrides = { + "aws" = "us-east-1" + } + } + + resource_prefix { + correct = "aws_mpa_" + } + + provider_package_correct = "mpa" + doc_prefix = ["mpa_"] + brand = "AWS" +} + service "mq" { sdk { id = "mq" diff --git a/website/allowed-subcategories.txt b/website/allowed-subcategories.txt index eb9fcefa2e8a..2a788c0d16aa 100644 --- a/website/allowed-subcategories.txt +++ b/website/allowed-subcategories.txt @@ -172,6 +172,7 @@ Managed Streaming for Kafka Managed Streaming for Kafka Connect MemoryDB Meta Data Sources +Multi-party Approval Neptune Neptune Analytics Network Firewall diff --git a/website/docs/guides/custom-service-endpoints.html.markdown b/website/docs/guides/custom-service-endpoints.html.markdown index ab4067a9e3e7..45f9651815f0 100644 --- a/website/docs/guides/custom-service-endpoints.html.markdown +++ b/website/docs/guides/custom-service-endpoints.html.markdown @@ -244,6 +244,7 @@ provider "aws" { |Elemental MediaStore|`mediastore`|`AWS_ENDPOINT_URL_MEDIASTORE`|`mediastore`| |MemoryDB|`memorydb`|`AWS_ENDPOINT_URL_MEMORYDB`|`memorydb`| |Application Migration (Mgn)|`mgn`|`AWS_ENDPOINT_URL_MGN`|`mgn`| +|Multi-party Approval|`mpa`|`AWS_ENDPOINT_URL_MPA`|`mpa`| |MQ|`mq`|`AWS_ENDPOINT_URL_MQ`|`mq`| |MWAA (Managed Workflows for Apache Airflow)|`mwaa`|`AWS_ENDPOINT_URL_MWAA`|`mwaa`| |MWAA (Managed Workflows for Apache Airflow) Serverless|`mwaaserverless`|`AWS_ENDPOINT_URL_MWAA_SERVERLESS`|`mwaa_serverless`|