Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting the User Agent Operator suffix #2831

Merged
merged 8 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions .changelog/2831.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:internal
provider: updated user agent string to now be `terraform-provider-cloudflare/<version> <plugin> <operator suffix>`
```

```release-note:enhancement
provider: allow defining a user agent operator suffix through the schema field (`user_agent_operator_suffix`) and via the environment variable (`CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX`)
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ resource "cloudflare_page_rule" "www" {
- `min_backoff` (Number) Minimum backoff period in seconds after failed API calls. Alternatively, can be configured using the `CLOUDFLARE_MIN_BACKOFF` environment variable.
- `retries` (Number) Maximum number of retries to perform when an API request fails. Alternatively, can be configured using the `CLOUDFLARE_RETRIES` environment variable.
- `rps` (Number) RPS limit to apply when making calls to the API. Alternatively, can be configured using the `CLOUDFLARE_RPS` environment variable.
- `user_agent_operator_suffix` (String) A value to append to the HTTP User Agent for all API calls. This value is not something most users need to modify however, if you are using a non-standard provider or operator configuration, this is recommended to assist in uniquely identifying your traffic. **Setting this value will remove the Terraform version from the HTTP User Agent string and may have unintended consequences**. Alternatively, can be configured using the `CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX` environment variable.
8 changes: 6 additions & 2 deletions internal/consts/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ const (
// Default value for the API base path.
APIBasePathDefault = "/client/v4"

// Schema key for the User Agent operator suffix.
UserAgentOperatorSuffixSchemaKey = "user_agent_operator_suffix"

// Environment variable key for the User Agent operator suffix.
UserAgentOperatorSuffixEnvVarKey = "CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX"

// Schema key for the requests per second configuration.
RPSSchemaKey = "rps"

Expand Down Expand Up @@ -87,8 +93,6 @@ const (
// Deprecated: Use resource specific account ID values instead.
AccountIDEnvVarKey = "CLOUDFLARE_ACCOUNT_ID"

UserAgentDefault = "terraform/%s terraform-plugin-sdk/%s terraform-provider-cloudflare/%s"

// Schema key for the account ID configuration.
AccountIDSchemaKey = "account_id"

Expand Down
43 changes: 29 additions & 14 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"github.com/hashicorp/terraform-plugin-mux/tf5to6server"
"github.com/hashicorp/terraform-plugin-mux/tf6muxserver"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
)

// Ensure CloudflareProvider satisfies various provider interfaces.
Expand All @@ -47,17 +46,18 @@ type CloudflareProvider struct {

// CloudflareProviderModel describes the provider data model.
type CloudflareProviderModel struct {
APIKey types.String `tfsdk:"api_key"`
APIUserServiceKey types.String `tfsdk:"api_user_service_key"`
Email types.String `tfsdk:"email"`
MinBackOff types.Int64 `tfsdk:"min_backoff"`
RPS types.Int64 `tfsdk:"rps"`
APIBasePath types.String `tfsdk:"api_base_path"`
APIToken types.String `tfsdk:"api_token"`
Retries types.Int64 `tfsdk:"retries"`
MaxBackoff types.Int64 `tfsdk:"max_backoff"`
APIClientLogging types.Bool `tfsdk:"api_client_logging"`
APIHostname types.String `tfsdk:"api_hostname"`
APIKey types.String `tfsdk:"api_key"`
APIUserServiceKey types.String `tfsdk:"api_user_service_key"`
Email types.String `tfsdk:"email"`
MinBackOff types.Int64 `tfsdk:"min_backoff"`
RPS types.Int64 `tfsdk:"rps"`
APIBasePath types.String `tfsdk:"api_base_path"`
APIToken types.String `tfsdk:"api_token"`
Retries types.Int64 `tfsdk:"retries"`
MaxBackoff types.Int64 `tfsdk:"max_backoff"`
APIClientLogging types.Bool `tfsdk:"api_client_logging"`
APIHostname types.String `tfsdk:"api_hostname"`
UserAgentOperatorSuffix types.String `tfsdk:"user_agent_operator_suffix"`
}

func (p *CloudflareProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
Expand Down Expand Up @@ -138,6 +138,11 @@ func (p *CloudflareProvider) Schema(ctx context.Context, req provider.SchemaRequ
Optional: true,
MarkdownDescription: fmt.Sprintf("Configure the base path used by the API client. Alternatively, can be configured using the `%s` environment variable.", consts.APIBasePathEnvVarKey),
},

consts.UserAgentOperatorSuffixSchemaKey: schema.StringAttribute{
Optional: true,
MarkdownDescription: fmt.Sprintf("A value to append to the HTTP User Agent for all API calls. This value is not something most users need to modify however, if you are using a non-standard provider or operator configuration, this is recommended to assist in uniquely identifying your traffic. **Setting this value will remove the Terraform version from the HTTP User Agent string and may have unintended consequences**. Alternatively, can be configured using the `%s` environment variable.", consts.UserAgentOperatorSuffixEnvVarKey),
},
},
}
}
Expand Down Expand Up @@ -234,8 +239,18 @@ func (p *CloudflareProvider) Configure(ctx context.Context, req provider.Configu

options = append(options, cloudflare.Debug(logging.IsDebugOrHigher()))

ua := fmt.Sprintf(consts.UserAgentDefault, req.TerraformVersion, meta.SDKVersionString(), p.version)
options = append(options, cloudflare.UserAgent(ua))
pluginVersion := utils.FindGoModuleVersion("github.com/hashicorp/terraform-plugin-framework")
userAgentParams := utils.UserAgentBuilderParams{
ProviderVersion: &p.version,
PluginType: cloudflare.StringPtr("terraform-plugin-framework"),
PluginVersion: pluginVersion,
}
if !data.UserAgentOperatorSuffix.IsNull() {
userAgentParams.OperatorSuffix = cloudflare.StringPtr(data.UserAgentOperatorSuffix.String())
} else {
userAgentParams.TerraformVersion = cloudflare.StringPtr(req.TerraformVersion)
}
options = append(options, cloudflare.UserAgent(userAgentParams.String()))

config := Config{Options: options}

Expand Down
21 changes: 18 additions & 3 deletions internal/sdkv2provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
)

func init() {
Expand Down Expand Up @@ -151,6 +150,12 @@ func New(version string) func() *schema.Provider {
Optional: true,
Description: fmt.Sprintf("Configure the base path used by the API client. Alternatively, can be configured using the `%s` environment variable.", consts.APIBasePathEnvVarKey),
},

consts.UserAgentOperatorSuffixSchemaKey: {
Type: schema.TypeString,
Optional: true,
Description: fmt.Sprintf("A value to append to the HTTP User Agent for all API calls. This value is not something most users need to modify however, if you are using a non-standard provider or operator configuration, this is recommended to assist in uniquely identifying your traffic. **Setting this value will remove the Terraform version from the HTTP User Agent string and may have unintended consequences**. Alternatively, can be configured using the `%s` environment variable.", consts.UserAgentOperatorSuffixEnvVarKey),
},
},

DataSourcesMap: map[string]*schema.Resource{
Expand Down Expand Up @@ -377,8 +382,18 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema

options = append(options, cloudflare.Debug(logging.IsDebugOrHigher()))

ua := fmt.Sprintf(consts.UserAgentDefault, p.TerraformVersion, meta.SDKVersionString(), version)
options = append(options, cloudflare.UserAgent(ua))
pluginVersion := utils.FindGoModuleVersion("github.com/hashicorp/terraform-plugin-sdk/v2")
userAgentParams := utils.UserAgentBuilderParams{
ProviderVersion: cloudflare.StringPtr(version),
PluginType: cloudflare.StringPtr("terraform-plugin-sdk"),
PluginVersion: pluginVersion,
}
if v, ok := d.GetOk(consts.UserAgentOperatorSuffixSchemaKey); ok {
userAgentParams.OperatorSuffix = cloudflare.StringPtr(v.(string))
} else {
userAgentParams.TerraformVersion = cloudflare.StringPtr(p.TerraformVersion)
}
options = append(options, cloudflare.UserAgent(userAgentParams.String()))

config := Config{Options: options}

Expand Down
33 changes: 33 additions & 0 deletions internal/utils/get_go_module_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package utils

import (
"runtime/debug"
"strings"

"github.com/cloudflare/cloudflare-go"
)

// FindGoModuleVersion digs into the build information and extracts the version
// of a module for use without the prefixed `v` (should it exist).
func FindGoModuleVersion(modulePath string) *string {
info, ok := debug.ReadBuildInfo()
if !ok {
// shouldn't ever happen but just in case we aren't using modules
return nil
}

for _, mod := range info.Deps {
if mod.Path != modulePath {
continue
}

version := mod.Version
if strings.HasPrefix(version, "v") {
version = strings.TrimPrefix(version, "v")
}

return cloudflare.StringPtr(version)
}

return nil
}
57 changes: 57 additions & 0 deletions internal/utils/user_agent_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package utils

import (
"fmt"
)

type UserAgentBuilderParams struct {
// Version of `terraform-provider-cloudflare`.
ProviderVersion *string

// Version of `terraform-plugin-*` libraries that we rely on for the internal
// operations.
PluginVersion *string

// Which plugin is in use. Currently only available options are
// `terraform-plugin-sdk` and `terraform-plugin-framework`.
PluginType *string

// Version of Terraform that is initiating the operation. Mutually exclusive
// with `OperatorSuffix`.
TerraformVersion *string

// Customised operation suffix to append to the user agent for identifying
// traffic. Mutually exclusive with `TerraformVersion`.
OperatorSuffix *string
}

func (p *UserAgentBuilderParams) String() string {
var ua string
if p.ProviderVersion != nil {
ua += fmt.Sprintf("terraform-provider-cloudflare/%s", *p.ProviderVersion)
}

if p.PluginType != nil {
ua += fmt.Sprintf(" %s", *p.PluginType)
}

if p.PluginVersion != nil {
ua += fmt.Sprintf("/%s", *p.PluginVersion)
}

// Operator suffix and Terraform version are mutually exclusive and we should
// only ever see one of them.
if p.OperatorSuffix != nil {
ua += fmt.Sprintf(" %s", *p.OperatorSuffix)
} else if p.TerraformVersion != nil {
ua += fmt.Sprintf(" terraform/%s", *p.TerraformVersion)
}

return ua
}

// BuildUserAgent takes the `UserAgentBuilderParams` and contextually builds
// a HTTP user agent for making API calls.
func BuildUserAgent(params UserAgentBuilderParams) string {
return params.String()
}
30 changes: 30 additions & 0 deletions internal/utils/user_agent_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package utils

import (
"reflect"
"testing"

"github.com/cloudflare/cloudflare-go"
)

func TestUserAgentBuilding(t *testing.T) {
tests := []struct {
input UserAgentBuilderParams
expect string
}{
{input: UserAgentBuilderParams{ProviderVersion: cloudflare.StringPtr("1.0")}, expect: "terraform-provider-cloudflare/1.0"},
{input: UserAgentBuilderParams{ProviderVersion: cloudflare.StringPtr("1.0"), PluginType: cloudflare.StringPtr("terraform-plugin-foo")}, expect: "terraform-provider-cloudflare/1.0 terraform-plugin-foo"},
{input: UserAgentBuilderParams{ProviderVersion: cloudflare.StringPtr("1.0"), PluginType: cloudflare.StringPtr("terraform-plugin-foo"), PluginVersion: cloudflare.StringPtr("1.2.3")}, expect: "terraform-provider-cloudflare/1.0 terraform-plugin-foo/1.2.3"},
{input: UserAgentBuilderParams{ProviderVersion: cloudflare.StringPtr("1.0"), PluginType: cloudflare.StringPtr("terraform-plugin-foo"), PluginVersion: cloudflare.StringPtr("1.2.3"), TerraformVersion: cloudflare.StringPtr("9.9.9")}, expect: "terraform-provider-cloudflare/1.0 terraform-plugin-foo/1.2.3 terraform/9.9.9"},
{input: UserAgentBuilderParams{ProviderVersion: cloudflare.StringPtr("1.0"), OperatorSuffix: cloudflare.StringPtr("example/v88")}, expect: "terraform-provider-cloudflare/1.0 example/v88"},
{input: UserAgentBuilderParams{ProviderVersion: cloudflare.StringPtr("1.0"), OperatorSuffix: cloudflare.StringPtr("example/v88"), TerraformVersion: cloudflare.StringPtr("1.2.3")}, expect: "terraform-provider-cloudflare/1.0 example/v88"},
{input: UserAgentBuilderParams{ProviderVersion: cloudflare.StringPtr("1.0"), TerraformVersion: cloudflare.StringPtr("1.2.3")}, expect: "terraform-provider-cloudflare/1.0 terraform/1.2.3"},
}

for _, tc := range tests {
got := BuildUserAgent(tc.input)
if !reflect.DeepEqual(tc.expect, got) {
t.Fatalf("expected: %v, got: %v", tc.expect, got)
}
}
}
Loading