Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changelog/45464.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
```release-note:enhancement
provider: The `provider_meta` block is now supported. The `user_agent` argument 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.
```
```release-note:enhancement
provider: Add `user_agent` argument
```
```release-note:new-function
user_agent
```
13 changes: 13 additions & 0 deletions internal/acctest/configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,19 @@ provider "aws" {
`, os.Getenv(envvar.AccAssumeRoleARN), policy)
}

// ConfigProviderMeta returns a terraform block with provider_meta configured
func ConfigProviderMeta() string {
return `
terraform {
provider_meta "aws" {
user_agent = [
"test-module/0.0.1 (test comment)",
]
}
}
`
}

const testAccProviderConfigBase = `
data "aws_region" "provider_test" {}
Expand Down
2 changes: 2 additions & 0 deletions internal/conns/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type Config struct {
TokenBucketRateLimiterCapacity int
UseDualStackEndpoint bool
UseFIPSEndpoint bool
UserAgent awsbase.UserAgentProducts
}

// ConfigureProvider configures the provided provider Meta (instance data).
Expand Down Expand Up @@ -114,6 +115,7 @@ func (c *Config) ConfigureProvider(ctx context.Context, client *AWSClient) (*AWS
TokenBucketRateLimiterCapacity: c.TokenBucketRateLimiterCapacity,
UseDualStackEndpoint: c.UseDualStackEndpoint,
UseFIPSEndpoint: c.UseFIPSEndpoint,
UserAgent: c.UserAgent,
}

if c.CustomCABundle != "" {
Expand Down
71 changes: 71 additions & 0 deletions internal/function/user_agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright IBM Corp. 2014, 2025
// SPDX-License-Identifier: MPL-2.0

package function

import (
"context"
"strings"

"github.com/hashicorp/terraform-plugin-framework/function"
)

var _ function.Function = userAgentFunction{}

func NewUserAgentFunction() function.Function {
return &userAgentFunction{}
}

type userAgentFunction struct{}

func (f userAgentFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "user_agent"
}

func (f userAgentFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "user_agent Function",
MarkdownDescription: "Formats a User-Agent product for use with the user_agent argument in the provider or provider_meta block.",
Parameters: []function.Parameter{
function.StringParameter{
Name: "product_name",
MarkdownDescription: "Product name.",
},
function.StringParameter{
Name: "product_version",
MarkdownDescription: "Product version.",
},
function.StringParameter{
Name: "comment",
MarkdownDescription: "Comment describing any additional product details.",
},
},
Return: function.StringReturn{},
}
}

func (f userAgentFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var name, version, comment string

resp.Error = function.ConcatFuncErrors(req.Arguments.Get(ctx, &name, &version, &comment))
if resp.Error != nil {
return
}

if name == "" {
resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError("product_name must be set"))
return
}

var sb strings.Builder

sb.WriteString(name)
if version != "" {
sb.WriteString("/" + version)
}
if comment != "" {
sb.WriteString(" (" + comment + ")")
}

resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, sb.String()))
}
118 changes: 118 additions & 0 deletions internal/function/user_agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright IBM Corp. 2014, 2025
// SPDX-License-Identifier: MPL-2.0

package function_test

import (
"fmt"
"testing"

"github.com/YakDriver/regexache"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
)

func TestUserAgentFunction_valid(t *testing.T) {
t.Parallel()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0"))),
},
Steps: []resource.TestStep{
{
Config: testUserAgentFunctionConfig("test-module", "0.0.1", "test comment"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckOutput("test", "test-module/0.0.1 (test comment)"),
),
},
},
})
}

func TestUserAgentFunction_valid_name(t *testing.T) {
t.Parallel()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0"))),
},
Steps: []resource.TestStep{
{
Config: testUserAgentFunctionConfig("test-module", "", ""),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckOutput("test", "test-module"),
),
},
},
})
}

func TestUserAgentFunction_valid_nameVersion(t *testing.T) {
t.Parallel()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0"))),
},
Steps: []resource.TestStep{
{
Config: testUserAgentFunctionConfig("test-module", "0.0.1", ""),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckOutput("test", "test-module/0.0.1"),
),
},
},
})
}

func TestUserAgentFunction_valid_nameComment(t *testing.T) {
t.Parallel()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0"))),
},
Steps: []resource.TestStep{
{
Config: testUserAgentFunctionConfig("test-module", "", "test comment"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckOutput("test", "test-module (test comment)"),
),
},
},
})
}

func TestUserAgentFunction_invalid(t *testing.T) {
t.Parallel()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0"))),
},
Steps: []resource.TestStep{
{
Config: testUserAgentFunctionConfig("", "", ""),
// The full message is broken across lines, complicating validation.
// Check just the start.
ExpectError: regexache.MustCompile("product_name must be"),
},
},
})
}

func testUserAgentFunctionConfig(name, version, comment string) string {
return fmt.Sprintf(`
output "test" {
value = provider::aws::user_agent(%[1]q, %[2]q, %[3]q)
}
`, name, version, comment)
}
20 changes: 20 additions & 0 deletions internal/provider/framework/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/function"
"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"
resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
Expand All @@ -49,6 +50,7 @@ var (
_ provider.ProviderWithFunctions = &frameworkProvider{}
_ provider.ProviderWithEphemeralResources = &frameworkProvider{}
_ provider.ProviderWithListResources = &frameworkProvider{}
_ provider.ProviderWithMetaSchema = &frameworkProvider{}
)

type frameworkProvider struct {
Expand Down Expand Up @@ -223,6 +225,11 @@ func (*frameworkProvider) Schema(ctx context.Context, request provider.SchemaReq
Optional: true,
Description: "Resolve an endpoint with FIPS capability",
},
"user_agent": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
Description: "Product details to append to the User-Agent string sent in all AWS API calls.",
},
},
Blocks: map[string]schema.Block{
"assume_role": schema.ListNestedBlock{
Expand Down Expand Up @@ -351,6 +358,18 @@ func (*frameworkProvider) Schema(ctx context.Context, request provider.SchemaReq
}
}

func (p *frameworkProvider) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) {
resp.Schema = metaschema.Schema{
Attributes: map[string]metaschema.Attribute{
"user_agent": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
Description: "Product details to append to the User-Agent string sent in all AWS API calls.",
},
},
}
}

// Configure is called at the beginning of the provider lifecycle, when
// Terraform sends to the provider the values the user specified in the
// provider configuration block.
Expand Down Expand Up @@ -406,6 +425,7 @@ func (p *frameworkProvider) Functions(_ context.Context) []func() function.Funct
tffunction.NewARNBuildFunction,
tffunction.NewARNParseFunction,
tffunction.NewTrimIAMRolePathFunction,
tffunction.NewUserAgentFunction,
}
}

Expand Down
Loading
Loading