diff --git a/CHANGELOG.md b/CHANGELOG.md index 226af158bd..61c3a3111d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 1.60.0 (Unreleased) +FEATURES: + +* provider: The `provider_meta` block is now supported. This enables module authors to include additional product information in the `User-Agent` header sent during all AWS API requests made during Create, Read, Update, and Delete operations. + ## 1.59.0 (October 9, 2025) BUG FIXES: diff --git a/docs/index.md b/docs/index.md index b023319b93..85da14a92b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -228,6 +228,37 @@ provider "awscc" { credential_process = custom-process --username jdoe ``` +### Module-scoped User-Agent Information with `provider_meta` + +The AWSCC provider supports sending provider metadata via the [`provider_meta` block](https://developer.hashicorp.com/terraform/internals/provider-meta). +This block allows module authors to provide additional information in the `User-Agent` header, scoped only to resources defined in a given module. + +For example, the following `terraform` block can be used to append additional User-Agent details. + +```terraform +terraform { + required_providers { + awscc = { + source = "hashicorp/awscc" + version = "~> 1.0" + } + } + + provider_meta "awscc" { + user_agent = [ + { + product_name = "example-demo" + product_version = "0.0.1" + comment = "a demo module" + }, + ] + } +} +``` + +Note that `provider_meta` is defined within the `terraform` block. +The `provider` block is inherited from the root module. + ## Schema @@ -251,7 +282,7 @@ credential_process = custom-process --username jdoe - `skip_medatadata_api_check` (Boolean, Deprecated) Skip the AWS Metadata API check. Useful for AWS API implementations that do not have a metadata API endpoint. Setting to `true` prevents Terraform from authenticating via the Metadata API. You may need to use other authentication methods like static credentials, configuration variables, or environment variables. - `skip_metadata_api_check` (Boolean) Skip the AWS Metadata API check. Useful for AWS API implementations that do not have a metadata API endpoint. Setting to `true` prevents Terraform from authenticating via the Metadata API. You may need to use other authentication methods like static credentials, configuration variables, or environment variables. - `token` (String) Session token for validating temporary credentials. Typically provided after successful identity federation or Multi-Factor Authentication (MFA) login. With MFA login, this is the session token provided afterward, not the 6 digit MFA code used to get temporary credentials. It can also be sourced from the `AWS_SESSION_TOKEN` environment variable. -- `user_agent` (Attributes List) Product details to append to User-Agent string in all AWS API calls. (see [below for nested schema](#nestedatt--user_agent)) +- `user_agent` (Attributes List) Product details to append to the User-Agent string sent in all AWS API calls. (see [below for nested schema](#nestedatt--user_agent)) ### Nested Schema for `assume_role` @@ -304,9 +335,9 @@ Optional: Required: -- `product_name` (String) Product name. At least one of `product_name` or `comment` must be set. +- `product_name` (String) Product name. Optional: -- `comment` (String) User-Agent comment. At least one of `comment` or `product_name` must be set. +- `comment` (String) Comment describing any additional product details. - `product_version` (String) Product version. Optional, and should only be set when `product_name` is set. diff --git a/internal/acctest/data.go b/internal/acctest/data.go index f5129f01ea..beb16f0e98 100644 --- a/internal/acctest/data.go +++ b/internal/acctest/data.go @@ -6,6 +6,7 @@ package acctest import ( "fmt" "os" + "strings" "testing" fwprovider "github.com/hashicorp/terraform-plugin-framework/provider" @@ -102,3 +103,36 @@ func NewTestData(_ *testing.T, cfResourceType, tfResourceType, resourceLabel str return data } + +// WithProviderMeta returns a terraform block with provider_meta configured +func WithProviderMeta() string { + return ` +terraform { + provider_meta "awscc" { + user_agent = [ + { + product_name = "test-module" + product_version = "0.0.1" + comment = "test comment" + }, + { + product_name = "second-test-module" + product_version = "0.0.2" + comment = "second test comment" + } + ] + } +} +` +} + +// ConfigCompose can be called to concatenate multiple strings to build test configurations +func ConfigCompose(config ...string) string { + var str strings.Builder + + for _, conf := range config { + str.WriteString(conf) + } + + return str.String() +} diff --git a/internal/aws/ec2/vpc_resource_test.go b/internal/aws/ec2/vpc_resource_test.go index 15d461a784..f187bdddd1 100644 --- a/internal/aws/ec2/vpc_resource_test.go +++ b/internal/aws/ec2/vpc_resource_test.go @@ -36,6 +36,31 @@ func TestAccAWSEC2VPC_CidrBlock(t *testing.T) { }) } +func TestAccAWSEC2VPC_providerMeta(t *testing.T) { + td := acctest.NewTestData(t, "AWS::EC2::VPC", "awscc_ec2_vpc", "test") + resourceName := td.ResourceName + rName := td.RandomName() + cidrBlock := "10.0.0.0/16" + + td.ResourceTest(t, []resource.TestStep{ + { + Config: acctest.ConfigCompose( + acctest.WithProviderMeta(), + testAccAWSEC2VPCCidrBlockConfig(&td, rName, cidrBlock), + ), + Check: resource.ComposeTestCheckFunc( + td.CheckExistsInAWS(), + resource.TestCheckResourceAttr(resourceName, "cidr_block", cidrBlock), + ), + }, + { + ResourceName: td.ResourceName, + ImportState: true, + ImportStateVerify: true, + }, + }) +} + func testAccAWSEC2VPCCidrBlockConfig(td *acctest.TestData, rName, cidrBlock string) string { return fmt.Sprintf(` resource %[1]q %[2]q { diff --git a/internal/aws/logs/log_group_resource_test.go b/internal/aws/logs/log_group_resource_test.go index a47f4c2c71..7b01301497 100644 --- a/internal/aws/logs/log_group_resource_test.go +++ b/internal/aws/logs/log_group_resource_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-awscc/internal/acctest" ) @@ -64,6 +65,49 @@ func TestAccAWSLogsLogGroupWithDataProtectionPolicy_create(t *testing.T) { }) } +func TestAccAWSLogsLogGroup_providerMeta(t *testing.T) { + td := acctest.NewTestData(t, "AWS::Logs::LogGroup", "awscc_logs_log_group", "test") + resourceName := td.ResourceName + rName := td.RandomName() + + td.ResourceTestNoProviderFactories(t, []resource.TestStep{ + { + ProtoV6ProviderFactories: td.ProviderFactories(), + ConfigDirectory: config.StaticDirectory("testdata/LogGroup/providerMeta/"), + ConfigVariables: config.Variables{ + "rName": config.StringVariable(rName), + }, + Check: resource.ComposeTestCheckFunc( + td.CheckExistsInAWS(), + resource.TestCheckResourceAttr(resourceName, "log_group_name", rName), + ), + }, + }) +} + +func TestAccAWSLogsLogGroup_providerMetaWithModule(t *testing.T) { + // https://github.com/hashicorp/terraform-plugin-testing/issues/277 + t.Skip("The ConfigDirectory field does not currently support copying modules to the working directory") + + td := acctest.NewTestData(t, "AWS::Logs::LogGroup", "awscc_logs_log_group", "test") + resourceName := td.ResourceName + rName := td.RandomName() + + td.ResourceTestNoProviderFactories(t, []resource.TestStep{ + { + ProtoV6ProviderFactories: td.ProviderFactories(), + ConfigDirectory: config.StaticDirectory("testdata/LogGroup/providerMetaWithModule/"), + ConfigVariables: config.Variables{ + "rName": config.StringVariable(rName), + }, + Check: resource.ComposeTestCheckFunc( + td.CheckExistsInAWS(), + resource.TestCheckResourceAttr(resourceName, "log_group_name", rName), + ), + }, + }) +} + func testAccAWSLogsLogGroupRetentionConfig(td *acctest.TestData, rName string, retentionInDays int) string { return fmt.Sprintf(` resource %[1]q %[2]q { diff --git a/internal/aws/logs/testdata/LogGroup/providerMeta/main.tf b/internal/aws/logs/testdata/LogGroup/providerMeta/main.tf new file mode 100644 index 0000000000..ef728de56a --- /dev/null +++ b/internal/aws/logs/testdata/LogGroup/providerMeta/main.tf @@ -0,0 +1,23 @@ +terraform { + provider_meta "awscc" { + user_agent = [ + { + product_name = "example-demo" + product_version = "0.0.1" + comment = "a demo module" + }, + ] + } +} + +resource "awscc_logs_log_group" "test" { + log_group_name = var.rName + retention_in_days = 7 +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + diff --git a/internal/aws/logs/testdata/LogGroup/providerMetaWithModule/demo-module/main.tf b/internal/aws/logs/testdata/LogGroup/providerMetaWithModule/demo-module/main.tf new file mode 100644 index 0000000000..f16322497e --- /dev/null +++ b/internal/aws/logs/testdata/LogGroup/providerMetaWithModule/demo-module/main.tf @@ -0,0 +1,23 @@ +terraform { + # custom provider_meta which should appear appended to user agent + provider_meta "awscc" { + user_agent = [ + { + product_name = "example-demo" + product_version = "0.0.1" + comment = "a demo module" + }, + ] + } +} + +resource "awscc_logs_log_group" "test_module" { + log_group_name = var.rName + retention_in_days = 7 +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} diff --git a/internal/aws/logs/testdata/LogGroup/providerMetaWithModule/main.tf b/internal/aws/logs/testdata/LogGroup/providerMetaWithModule/main.tf new file mode 100644 index 0000000000..9f238328f1 --- /dev/null +++ b/internal/aws/logs/testdata/LogGroup/providerMetaWithModule/main.tf @@ -0,0 +1,16 @@ +module "demo-module" { + source = "./demo-module" + rName = var.rName +} + +resource "awscc_logs_log_group" "test" { + log_group_name = var.rName + retention_in_days = 7 +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + diff --git a/internal/generic/resource.go b/internal/generic/resource.go index cdb00f153b..944fb861f1 100644 --- a/internal/generic/resource.go +++ b/internal/generic/resource.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudcontrol" cctypes "github.com/aws/aws-sdk-go-v2/service/cloudcontrol/types" + "github.com/hashicorp/aws-sdk-go-base/v2/useragent" hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -26,6 +27,7 @@ import ( "github.com/hashicorp/terraform-provider-awscc/internal/identity" tfcloudcontrol "github.com/hashicorp/terraform-provider-awscc/internal/service/cloudcontrol" "github.com/hashicorp/terraform-provider-awscc/internal/tfresource" + inttypes "github.com/hashicorp/terraform-provider-awscc/internal/types" ) // ResourceOptionsFunc is a type alias for a resource type functional option. @@ -368,6 +370,10 @@ var ( idAttributePath = path.Root("id") ) +type providerMetaData struct { + UserAgent types.List `tfsdk:"user_agent"` +} + func (r *genericResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { response.TypeName = r.tfTypeName @@ -387,7 +393,10 @@ func (r *genericResource) Configure(_ context.Context, request resource.Configur } func (r *genericResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { - ctx = r.bootstrapContext(ctx) + ctx = r.bootstrapContextWithProviderMeta(ctx, request.ProviderMeta, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } traceEntry(ctx, "Resource.Create") @@ -490,7 +499,10 @@ func (r *genericResource) Create(ctx context.Context, request resource.CreateReq } func (r *genericResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { - ctx = r.bootstrapContext(ctx) + ctx = r.bootstrapContextWithProviderMeta(ctx, request.ProviderMeta, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } traceEntry(ctx, "Resource.Read") @@ -574,7 +586,10 @@ func (r *genericResource) Read(ctx context.Context, request resource.ReadRequest } func (r *genericResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - ctx = r.bootstrapContext(ctx) + ctx = r.bootstrapContextWithProviderMeta(ctx, request.ProviderMeta, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } traceEntry(ctx, "Resource.Update") @@ -712,7 +727,10 @@ func (r *genericResource) Update(ctx context.Context, request resource.UpdateReq } func (r *genericResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { - ctx = r.bootstrapContext(ctx) + ctx = r.bootstrapContextWithProviderMeta(ctx, request.ProviderMeta, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } traceEntry(ctx, "Resource.Delete") @@ -926,10 +944,25 @@ func (r *genericResource) populateUnknownValues(ctx context.Context, id string, return nil } -// bootstrapContext injects the CloudFormation type name into logger contexts. +// bootstrapContext injects the CloudFormation type name into logger contexts func (r *genericResource) bootstrapContext(ctx context.Context) context.Context { ctx = tflog.SetField(ctx, LoggingKeyCFNType, r.cfTypeName) - ctx = r.provider.RegisterLogger(ctx) + return r.provider.RegisterLogger(ctx) +} + +// bootstrapContextWithProviderMeta is an extension of bootstrapContext which +// also injects details from the provider_meta block into context +func (r *genericResource) bootstrapContextWithProviderMeta(ctx context.Context, providerMeta tfsdk.Config, d *diag.Diagnostics) context.Context { + ctx = r.bootstrapContext(ctx) + + var metadata *providerMetaData + d.Append(providerMeta.Get(ctx, &metadata)...) + + if metadata != nil { + var uap inttypes.UserAgentProducts + d.Append(metadata.UserAgent.ElementsAs(ctx, &uap, false)...) + ctx = useragent.Context(ctx, uap.UserAgentProducts()) + } return ctx } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 707eadda78..67991ac6ea 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -19,13 +19,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-provider-awscc/internal/flex" "github.com/hashicorp/terraform-provider-awscc/internal/registry" - cctypes "github.com/hashicorp/terraform-provider-awscc/internal/types" + inttypes "github.com/hashicorp/terraform-provider-awscc/internal/types" ) const ( @@ -68,6 +69,10 @@ func (p *providerData) RoleARN(_ context.Context) string { return p.roleARN } +var _ provider.Provider = &ccProvider{} +var _ provider.ProviderWithMetaSchema = &ccProvider{} +var _ provider.ProviderWithListResources = &ccProvider{} + type ccProvider struct { providerData *providerData // Used in acceptance tests. } @@ -96,7 +101,7 @@ func (p *ccProvider) Schema(ctx context.Context, request provider.SchemaRequest, "assume_role": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "duration": schema.StringAttribute{ - CustomType: cctypes.DurationType, + CustomType: inttypes.DurationType, Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", Optional: true, }, @@ -110,12 +115,12 @@ func (p *ccProvider) Schema(ctx context.Context, request provider.SchemaRequest, Optional: true, }, "policy_arns": schema.ListAttribute{ - ElementType: cctypes.ARNType, + ElementType: inttypes.ARNType, Description: "Amazon Resource Names (ARNs) of IAM Policies to use as managed session policies. The effective permissions for the session will be the intersection between these polcy and the role's policies.", Optional: true, }, "role_arn": schema.StringAttribute{ - CustomType: cctypes.ARNType, + CustomType: inttypes.ARNType, Description: "Amazon Resource Name (ARN) of the IAM Role to assume.", Required: true, }, @@ -140,7 +145,7 @@ func (p *ccProvider) Schema(ctx context.Context, request provider.SchemaRequest, "assume_role_with_web_identity": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "duration": schema.StringAttribute{ - CustomType: cctypes.DurationType, + CustomType: inttypes.DurationType, Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", Optional: true, }, @@ -150,12 +155,12 @@ func (p *ccProvider) Schema(ctx context.Context, request provider.SchemaRequest, Optional: true, }, "policy_arns": schema.ListAttribute{ - ElementType: cctypes.ARNType, + ElementType: inttypes.ARNType, Description: "Amazon Resource Names (ARNs) of IAM Policies to use as managed session policies. The effective permissions for the session will be the intersection between these polcy and the role's policies.", Optional: true, }, "role_arn": schema.StringAttribute{ - CustomType: cctypes.ARNType, + CustomType: inttypes.ARNType, Description: "Amazon Resource Name (ARN) of the IAM Role to assume. Can also be set with the environment variable `AWS_ROLE_ARN`.", Required: true, }, @@ -229,7 +234,7 @@ func (p *ccProvider) Schema(ctx context.Context, request provider.SchemaRequest, Optional: true, }, "role_arn": schema.StringAttribute{ - CustomType: cctypes.ARNType, + CustomType: inttypes.ARNType, Description: "Amazon Resource Name of the AWS CloudFormation service role that is used on your behalf to perform operations.", Optional: true, }, @@ -264,11 +269,38 @@ func (p *ccProvider) Schema(ctx context.Context, request provider.SchemaRequest, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "comment": schema.StringAttribute{ - Description: "User-Agent comment. At least one of `comment` or `product_name` must be set.", + Description: "Comment describing any additional product details.", + Optional: true, + }, + "product_name": schema.StringAttribute{ + Description: "Product name.", + Required: true, + }, + "product_version": schema.StringAttribute{ + Description: "Product version. Optional, and should only be set when `product_name` is set.", + Optional: true, + }, + }, + }, + Description: "Product details to append to the User-Agent string sent in all AWS API calls.", + Optional: true, + }, + }, + } +} + +func (p *ccProvider) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "user_agent": metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "comment": schema.StringAttribute{ + Description: "Comment describing any additional product details.", Optional: true, }, "product_name": schema.StringAttribute{ - Description: "Product name. At least one of `product_name` or `comment` must be set.", + Description: "Product name.", Required: true, }, "product_version": schema.StringAttribute{ @@ -277,7 +309,7 @@ func (p *ccProvider) Schema(ctx context.Context, request provider.SchemaRequest, }, }, }, - Description: "Product details to append to User-Agent string in all AWS API calls.", + Description: "Product details to append to the User-Agent string sent in all AWS API calls.", Optional: true, }, }, @@ -296,33 +328,27 @@ type configModel struct { NoProxy types.String `tfsdk:"no_proxy"` Profile types.String `tfsdk:"profile"` Region types.String `tfsdk:"region"` - RoleARN cctypes.ARN `tfsdk:"role_arn"` + RoleARN inttypes.ARN `tfsdk:"role_arn"` SecretKey types.String `tfsdk:"secret_key"` SharedConfigFiles types.List `tfsdk:"shared_config_files"` SharedCredentialsFiles types.List `tfsdk:"shared_credentials_files"` SkipMedatadataApiCheck types.Bool `tfsdk:"skip_medatadata_api_check"` SkipMetadataApiCheck types.Bool `tfsdk:"skip_metadata_api_check"` Token types.String `tfsdk:"token"` - UserAgent []userAgentProduct `tfsdk:"user_agent"` + UserAgent inttypes.UserAgentProducts `tfsdk:"user_agent"` terraformVersion string } -type userAgentProduct struct { - Comment types.String `tfsdk:"comment"` - ProductName types.String `tfsdk:"product_name"` - ProductVersion types.String `tfsdk:"product_version"` -} - type assumeRoleModel struct { - Duration cctypes.Duration `tfsdk:"duration"` - ExternalID types.String `tfsdk:"external_id"` - Policy jsontypes.Exact `tfsdk:"policy"` - PolicyARNs types.List `tfsdk:"policy_arns"` - RoleARN cctypes.ARN `tfsdk:"role_arn"` - SessionName types.String `tfsdk:"session_name"` - Tags types.Map `tfsdk:"tags"` - TransitiveTagKeys types.Set `tfsdk:"transitive_tag_keys"` + Duration inttypes.Duration `tfsdk:"duration"` + ExternalID types.String `tfsdk:"external_id"` + Policy jsontypes.Exact `tfsdk:"policy"` + PolicyARNs types.List `tfsdk:"policy_arns"` + RoleARN inttypes.ARN `tfsdk:"role_arn"` + SessionName types.String `tfsdk:"session_name"` + Tags types.Map `tfsdk:"tags"` + TransitiveTagKeys types.Set `tfsdk:"transitive_tag_keys"` } func (a assumeRoleModel) Config() awsbase.AssumeRole { @@ -366,13 +392,13 @@ type endpointData struct { } type assumeRoleWithWebIdentityData struct { - Duration cctypes.Duration `tfsdk:"duration"` - Policy jsontypes.Exact `tfsdk:"policy"` - PolicyARNs types.List `tfsdk:"policy_arns"` - RoleARN cctypes.ARN `tfsdk:"role_arn"` - SessionName types.String `tfsdk:"session_name"` - WebIdentityToken types.String `tfsdk:"web_identity_token"` - WebIdentityTokenFile types.String `tfsdk:"web_identity_token_file"` + Duration inttypes.Duration `tfsdk:"duration"` + Policy jsontypes.Exact `tfsdk:"policy"` + PolicyARNs types.List `tfsdk:"policy_arns"` + RoleARN inttypes.ARN `tfsdk:"role_arn"` + SessionName types.String `tfsdk:"session_name"` + WebIdentityToken types.String `tfsdk:"web_identity_token"` + WebIdentityTokenFile types.String `tfsdk:"web_identity_token_file"` } func (a assumeRoleWithWebIdentityData) Config() *awsbase.AssumeRoleWithWebIdentity { @@ -520,7 +546,8 @@ func newProviderData(ctx context.Context, c *configModel) (*providerData, diag.D }, }, } - awsbaseConfig.UserAgent = userAgentProducts(c.UserAgent) + + awsbaseConfig.UserAgent = c.UserAgent.UserAgentProducts() if c.MaxRetries.IsNull() { awsbaseConfig.MaxRetries = defaultMaxRetries } else { @@ -614,15 +641,3 @@ func newProviderData(ctx context.Context, c *configModel) (*providerData, diag.D return providerData, diags } - -func userAgentProducts(products []userAgentProduct) []awsbase.UserAgentProduct { - results := make([]awsbase.UserAgentProduct, len(products)) - for i, p := range products { - results[i] = awsbase.UserAgentProduct{ - Name: p.ProductName.ValueString(), - Version: p.ProductVersion.ValueString(), - Comment: p.Comment.ValueString(), - } - } - return results -} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 06345e20b2..b59b20e8da 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -5,52 +5,6 @@ package provider import ( "testing" - - "github.com/google/go-cmp/cmp" - awsbase "github.com/hashicorp/aws-sdk-go-base/v2" - "github.com/hashicorp/terraform-plugin-framework/types" ) func TestProvider(t *testing.T) {} - -func TestUserAgentProducts(t *testing.T) { - t.Parallel() - - simpleProduct := awsbase.UserAgentProduct{Name: "simple", Version: "t", Comment: "t"} - simpleAddProduct := userAgentProduct{ProductName: types.StringValue(simpleProduct.Name), ProductVersion: types.StringValue(simpleProduct.Version), Comment: types.StringValue(simpleProduct.Comment)} - minimalProduct := awsbase.UserAgentProduct{Name: "minimal"} - minimalAddProduct := userAgentProduct{ProductName: types.StringValue(minimalProduct.Name)} - - testcases := map[string]struct { - addProducts []userAgentProduct - expected []awsbase.UserAgentProduct - }{ - "none_added": { - addProducts: []userAgentProduct{}, - expected: []awsbase.UserAgentProduct{}, - }, - "simple_added": { - addProducts: []userAgentProduct{simpleAddProduct}, - expected: []awsbase.UserAgentProduct{simpleProduct}, - }, - "minimal_added": { - addProducts: []userAgentProduct{minimalAddProduct}, - expected: []awsbase.UserAgentProduct{minimalProduct}, - }, - "both_added": { - addProducts: []userAgentProduct{simpleAddProduct, minimalAddProduct}, - expected: []awsbase.UserAgentProduct{simpleProduct, minimalProduct}, - }, - } - - for name, testcase := range testcases { - name, testcase := name, testcase - - t.Run(name, func(t *testing.T) { - actual := userAgentProducts(testcase.addProducts) - if !cmp.Equal(testcase.expected, actual) { - t.Errorf("expected %q, got %q", testcase.expected, actual) - } - }) - } -} diff --git a/internal/types/user_agent.go b/internal/types/user_agent.go new file mode 100644 index 0000000000..2070b641c3 --- /dev/null +++ b/internal/types/user_agent.go @@ -0,0 +1,26 @@ +package types + +import ( + awsbase "github.com/hashicorp/aws-sdk-go-base/v2" + tftypes "github.com/hashicorp/terraform-plugin-framework/types" +) + +type userAgentProduct struct { + Comment tftypes.String `tfsdk:"comment"` + ProductName tftypes.String `tfsdk:"product_name"` + ProductVersion tftypes.String `tfsdk:"product_version"` +} + +type UserAgentProducts []userAgentProduct + +func (uap UserAgentProducts) UserAgentProducts() []awsbase.UserAgentProduct { + results := make([]awsbase.UserAgentProduct, len(uap)) + for i, p := range uap { + results[i] = awsbase.UserAgentProduct{ + Comment: p.Comment.ValueString(), + Name: p.ProductName.ValueString(), + Version: p.ProductVersion.ValueString(), + } + } + return results +} diff --git a/internal/types/user_agent_test.go b/internal/types/user_agent_test.go new file mode 100644 index 0000000000..bd8f3dad3d --- /dev/null +++ b/internal/types/user_agent_test.go @@ -0,0 +1,57 @@ +package types + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + awsbase "github.com/hashicorp/aws-sdk-go-base/v2" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestUserAgentProducts(t *testing.T) { + t.Parallel() + + simpleProduct := awsbase.UserAgentProduct{Name: "simple", Version: "0.0.1", Comment: "test comment"} + simpleAddProduct := userAgentProduct{ + ProductName: types.StringValue(simpleProduct.Name), + ProductVersion: types.StringValue(simpleProduct.Version), + Comment: types.StringValue(simpleProduct.Comment), + } + minimalProduct := awsbase.UserAgentProduct{Name: "minimal"} + minimalAddProduct := userAgentProduct{ + ProductName: types.StringValue(minimalProduct.Name), + } + + testcases := map[string]struct { + add UserAgentProducts + expected []awsbase.UserAgentProduct + }{ + "none": { + add: []userAgentProduct{}, + expected: []awsbase.UserAgentProduct{}, + }, + "simple": { + add: []userAgentProduct{simpleAddProduct}, + expected: []awsbase.UserAgentProduct{simpleProduct}, + }, + "minimal": { + add: []userAgentProduct{minimalAddProduct}, + expected: []awsbase.UserAgentProduct{minimalProduct}, + }, + "both": { + add: []userAgentProduct{simpleAddProduct, minimalAddProduct}, + expected: []awsbase.UserAgentProduct{simpleProduct, minimalProduct}, + }, + } + + for name, testcase := range testcases { + name, testcase := name, testcase + + t.Run(name, func(t *testing.T) { + actual := testcase.add.UserAgentProducts() + if !cmp.Equal(testcase.expected, actual) { + t.Errorf("expected %q, got %q", testcase.expected, actual) + } + }) + } +} diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index 91cad03fdd..7e0006c95f 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -228,4 +228,35 @@ provider "awscc" { credential_process = custom-process --username jdoe ``` +### Module-scoped User-Agent Information with `provider_meta` + +The AWSCC provider supports sending provider metadata via the [`provider_meta` block](https://developer.hashicorp.com/terraform/internals/provider-meta). +This block allows module authors to provide additional information in the `User-Agent` header, scoped only to resources defined in a given module. + +For example, the following `terraform` block can be used to append additional User-Agent details. + +```terraform +terraform { + required_providers { + awscc = { + source = "hashicorp/awscc" + version = "~> 1.0" + } + } + + provider_meta "awscc" { + user_agent = [ + { + product_name = "example-demo" + product_version = "0.0.1" + comment = "a demo module" + }, + ] + } +} +``` + +Note that `provider_meta` is defined within the `terraform` block. +The `provider` block is inherited from the root module. + {{ .SchemaMarkdown | trimspace }}