Skip to content

Commit

Permalink
Merge pull request #2831 from cloudflare/allow-ua-operator-appending
Browse files Browse the repository at this point in the history
Allow setting the User Agent Operator suffix
  • Loading branch information
jacobbednarz authored Oct 13, 2023
2 parents 126b76f + 13633cf commit d9e8f42
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 19 deletions.
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)
}
}
}

0 comments on commit d9e8f42

Please sign in to comment.