Skip to content

Commit 07a1a5f

Browse files
jkodroffclaude
andcommitted
Identify Pulumi in the Cloudflare User-Agent
Cloudflare could not attribute API traffic to Pulumi, so provider usage looked like plain Terraform usage. Upstream built the hook for us back in 2023 (cloudflare/terraform-provider-cloudflare#2831) - a provider config attribute `user_agent_operator_suffix` that replaces the `terraform/<version>` token - but the Pulumi side was never wired up. Default `userAgentOperatorSuffix` to `pulumi/<version>` from a PreConfigureCallback. Requests now go out as: terraform-provider-cloudflare/pulumi terraform-plugin-framework/<v> pulumi/<v> Precedence is explicit config, then CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX, then the default. An explicitly empty value in either removes the key, which opts out by restoring upstream's `terraform/<version>` default - so this needs no new config surface. Setting the suffix from a callback rather than `Config[...].Default` keeps the version out of the generated schema. `Default.Value` is copied verbatim into schema.json and every SDK as a client-side literal, which would churn the schema on every release and leave an SDK reporting whichever version it was built at. Two upstream bugs sit in this path, both still present on main and now tracked in cloudflare/terraform-provider-cloudflare#7323: - `Configure` formats the suffix with `types.String.String()`, which is `fmt.Sprintf("%q", ...)`, so it would go out double-quoted. - The `IsNull()` guard does not cover `IsUnknown()`, so an unknown value would go out as the literal `<unknown>`. patch 0002 fixes both and is written to be upstreamable as-is. patch 0003 is Pulumi-specific: it rewrites the config description, which would otherwise have claimed the default is the Terraform version, and adds the option to upstream's docs/index.md, which omitted it entirely. The shim still passes the literal string "pulumi" where upstream expects a semver. That is pre-existing, it also feeds x-stainless-package-version, and every obvious fix has a downside - see #1655. Fixes #179 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent dc5179f commit 07a1a5f

20 files changed

Lines changed: 390 additions & 28 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ The following configuration points are available:
5353
- `cloudflare:apiUserServiceKey` - (Optional) A special Cloudflare API key good for a restricted set of endpoints. Alternatively, can be configured using the `CLOUDFLARE_API_USER_SERVICE_KEY` environment variable. Must provide only one of `cloudflare:apiKey`, `cloudflare:apiToken`, `cloudflare:apiUserServiceKey`.
5454
- `cloudflare:baseUrl` (String) Value to override the default HTTP client base URL. Alternatively, can be configured using the `CLOUDFLARE_BASE_URL` environment variable.
5555
- `cloudflare:email` - (Optional) A registered Cloudflare email address. Alternatively, can be configured using the `CLOUDFLARE_EMAIL` environment variable. Required when using `cloudflare:apiKey`. Conflicts with `cloudflare:apiToken`.
56-
- `cloudflare:userAgentOperatorSuffix` - (Optional) 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 Pulumi 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.
56+
- `cloudflare:userAgentOperatorSuffix` - (Optional) A value appended to the HTTP User Agent sent with every API call, used to identify the tool making the request. Defaults to `pulumi/<version>`, which allows Cloudflare to better understand usage of the provider; it does not give Pulumi any access to your account or to usage information. Most users do not need to change this. Set it to a value of your own to identify your traffic differently, or to the empty string to remove the Pulumi identifier entirely. Alternatively, can be configured using the `CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX` environment variable.
5757

5858
## Reference
5959

docs/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,5 @@ resource "cloudflare_dnsrecord" "www" {
203203
- `apiKey` (String, Sensitive) The API key for operations. Alternatively, can be configured using the `CLOUDFLARE_API_KEY` environment variable. API keys are [now considered legacy by Cloudflare](https://developers.cloudflare.com/fundamentals/api/get-started/keys/#limitations), API tokens should be used instead. Must provide only one of `apiKey`, `apiToken`, `userServiceKey`.
204204
- `apiToken` (String, Sensitive) The API Token for operations. Alternatively, can be configured using the `CLOUDFLARE_API_TOKEN` environment variable. Must provide only one of `apiKey`, `apiToken`, `userServiceKey`.
205205
- `baseUrl` (String) Set the base url that the provider connects to. Alternatively, can be configured using the `CLOUDFLARE_BASE_URL` environment variable.
206+
- `userAgentOperatorSuffix` (String) A value appended to the HTTP User Agent sent with every API call, used to identify the tool making the request. Defaults to `pulumi/<version>`, which allows Cloudflare to better understand usage of the provider; it does not give Pulumi any access to your account or to usage information. Most users do not need to change this. Set it to a value of your own to identify your traffic differently, or to the empty string to remove the Pulumi identifier entirely. Alternatively, can be configured using the `CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX` environment variable.
206207
- `userServiceKey` (String, Sensitive) A special Cloudflare API key good for a restricted set of endpoints. Alternatively, can be configured using the `CLOUDFLARE_API_USER_SERVICE_KEY` environment variable. Must provide only one of `apiKey`, `apiToken`, `userServiceKey`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2+
From: Josh Kodroff <josh@pulumi.com>
3+
Date: Wed, 19 Aug 2026 17:32:05 -0400
4+
Subject: [PATCH] Use ValueString for user_agent_operator_suffix
5+
6+
7+
diff --git a/internal/provider.go b/internal/provider.go
8+
index bc14b3bbb..223362592 100644
9+
--- a/internal/provider.go
10+
+++ b/internal/provider.go
11+
@@ -432,8 +432,8 @@ func (p *CloudflareProvider) Configure(ctx context.Context, req provider.Configu
12+
PluginVersion: pluginVersion,
13+
}
14+
15+
- if !data.UserAgentOperatorSuffix.IsNull() {
16+
- operatorSuffix := data.UserAgentOperatorSuffix.String()
17+
+ if !data.UserAgentOperatorSuffix.IsNull() && !data.UserAgentOperatorSuffix.IsUnknown() {
18+
+ operatorSuffix := data.UserAgentOperatorSuffix.ValueString()
19+
userAgentParams.OperatorSuffix = &operatorSuffix
20+
} else {
21+
userAgentParams.TerraformVersion = &req.TerraformVersion
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2+
From: Josh Kodroff <josh@pulumi.com>
3+
Date: Wed, 19 Aug 2026 17:32:19 -0400
4+
Subject: [PATCH] Describe the Pulumi user agent suffix default
5+
6+
7+
diff --git a/docs/index.md b/docs/index.md
8+
index 8d321b813..420a8708d 100644
9+
--- a/docs/index.md
10+
+++ b/docs/index.md
11+
@@ -49,4 +49,5 @@ resource "cloudflare_dns_record" "www" {
12+
- `api_key` (String, Sensitive) The API key for operations. Alternatively, can be configured using the `CLOUDFLARE_API_KEY` environment variable. API keys are [now considered legacy by Cloudflare](https://developers.cloudflare.com/fundamentals/api/get-started/keys/#limitations), API tokens should be used instead. Must provide only one of `api_key`, `api_token`, `user_service_key`.
13+
- `api_token` (String, Sensitive) The API Token for operations. Alternatively, can be configured using the `CLOUDFLARE_API_TOKEN` environment variable. Must provide only one of `api_key`, `api_token`, `user_service_key`.
14+
- `base_url` (String) Set the base url that the provider connects to. Alternatively, can be configured using the `CLOUDFLARE_BASE_URL` environment variable.
15+
+- `user_agent_operator_suffix` (String) A value appended to the HTTP User Agent sent with every API call, used to identify the tool making the request. Defaults to `pulumi/<version>`, which allows Cloudflare to better understand usage of the provider; it does not give Pulumi any access to your account or to usage information. Most users do not need to change this. Set it to a value of your own to identify your traffic differently, or to the empty string to remove the Pulumi identifier entirely. Alternatively, can be configured using the `CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX` environment variable.
16+
- `user_service_key` (String, Sensitive) A special Cloudflare API key good for a restricted set of endpoints. Alternatively, can be configured using the `CLOUDFLARE_API_USER_SERVICE_KEY` environment variable. Must provide only one of `api_key`, `api_token`, `user_service_key`.
17+
diff --git a/internal/provider.go b/internal/provider.go
18+
index 223362592..cc8b870ea 100644
19+
--- a/internal/provider.go
20+
+++ b/internal/provider.go
21+
@@ -371,7 +371,7 @@ func ProviderSchema(ctx context.Context) schema.Schema {
22+
23+
consts.UserAgentOperatorSuffixSchemaKey: schema.StringAttribute{
24+
Optional: true,
25+
- 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),
26+
+ MarkdownDescription: fmt.Sprintf("A value appended to the HTTP User Agent sent with every API call, used to identify the tool making the request. Defaults to `pulumi/<version>`, which allows Cloudflare to better understand usage of the provider; it does not give Pulumi any access to your account or to usage information. Most users do not need to change this. Set it to a value of your own to identify your traffic differently, or to the empty string to remove the Pulumi identifier entirely. Alternatively, can be configured using the `%s` environment variable.", consts.UserAgentOperatorSuffixEnvVarKey),
27+
},
28+
29+
consts.BaseURLSchemaKey: schema.StringAttribute{

provider/cmd/pulumi-resource-cloudflare/schema.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
},
7878
"userAgentOperatorSuffix": {
7979
"type": "string",
80-
"description": "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."
80+
"description": "A value appended to the HTTP User Agent sent with every API call, used to identify the tool making the request. Defaults to `pulumi/\u003cversion\u003e`, which allows Cloudflare to better understand usage of the provider; it does not give Pulumi any access to your account or to usage information. Most users do not need to change this. Set it to a value of your own to identify your traffic differently, or to the empty string to remove the Pulumi identifier entirely. Alternatively, can be configured using the `CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX` environment variable."
8181
}
8282
}
8383
},
@@ -91898,7 +91898,7 @@
9189891898
},
9189991899
"userAgentOperatorSuffix": {
9190091900
"type": "string",
91901-
"description": "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."
91901+
"description": "A value appended to the HTTP User Agent sent with every API call, used to identify the tool making the request. Defaults to `pulumi/\u003cversion\u003e`, which allows Cloudflare to better understand usage of the provider; it does not give Pulumi any access to your account or to usage information. Most users do not need to change this. Set it to a value of your own to identify your traffic differently, or to the empty string to remove the Pulumi identifier entirely. Alternatively, can be configured using the `CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX` environment variable."
9190291902
}
9190391903
},
9190491904
"inputProperties": {
@@ -91927,7 +91927,7 @@
9192791927
},
9192891928
"userAgentOperatorSuffix": {
9192991929
"type": "string",
91930-
"description": "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."
91930+
"description": "A value appended to the HTTP User Agent sent with every API call, used to identify the tool making the request. Defaults to `pulumi/\u003cversion\u003e`, which allows Cloudflare to better understand usage of the provider; it does not give Pulumi any access to your account or to usage information. Most users do not need to change this. Set it to a value of your own to identify your traffic differently, or to the empty string to remove the Pulumi identifier entirely. Alternatively, can be configured using the `CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX` environment variable."
9193191931
}
9193291932
},
9193391933
"methods": {

provider/resources.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info"
3131
tfbridgetokens "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/tokens"
3232
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen"
33+
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
3334
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
3435

3536
"github.com/pulumi/pulumi-cloudflare/provider/v6/pkg/version"
@@ -81,6 +82,10 @@ func Provider() info.Provider {
8182
TFProviderModuleVersion: "v5",
8283
MetadataInfo: tfbridge.NewProviderMetadata(metadata),
8384
DocRules: &info.DocRule{EditRules: docEditRules},
85+
PreConfigureCallback: func(vars resource.PropertyMap, _ shim.ResourceConfig) error {
86+
setUserAgentOperatorSuffix(vars)
87+
return nil
88+
},
8489
Config: map[string]*info.Schema{
8590
"api_token": {
8691
Secret: tfbridge.True(),

provider/user_agent.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package cloudflare
2+
3+
import (
4+
"os"
5+
6+
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
7+
8+
"github.com/pulumi/pulumi-cloudflare/provider/v6/pkg/version"
9+
)
10+
11+
// userAgentOperatorSuffixKey is the Pulumi name of upstream's `user_agent_operator_suffix` provider
12+
// config attribute. PreConfigureCallback receives provider config keyed by Pulumi names, not
13+
// Terraform names.
14+
const userAgentOperatorSuffixKey = resource.PropertyKey("userAgentOperatorSuffix")
15+
16+
// userAgentOperatorSuffixEnvVar is documented by upstream's schema but never actually read by its
17+
// Configure implementation in terraform-provider-cloudflare v5, so we honor it ourselves.
18+
const userAgentOperatorSuffixEnvVar = "CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX"
19+
20+
// pulumiUserAgentOperatorSuffix is the identifier we send to Cloudflare by default.
21+
func pulumiUserAgentOperatorSuffix() string {
22+
// version.Version is populated by -ldflags at build time, so it is only ever empty in `go test`
23+
// and `go run` builds.
24+
if version.Version == "" {
25+
return "pulumi"
26+
}
27+
return "pulumi/" + version.Version
28+
}
29+
30+
// setUserAgentOperatorSuffix makes Cloudflare API requests identify themselves as coming from
31+
// Pulumi, so that Cloudflare can attribute traffic to the provider rather than to Terraform.
32+
//
33+
// Upstream composes the User-Agent as
34+
//
35+
// terraform-provider-cloudflare/<v> terraform-plugin-framework/<v> <suffix>
36+
//
37+
// where <suffix> is the operator suffix if one is set, and `terraform/<version>` otherwise. The
38+
// latter is meaningless under Pulumi - the bridge synthesizes a fake Terraform version - so we
39+
// always prefer the suffix.
40+
//
41+
// Precedence is explicit config, then CLOUDFLARE_USER_AGENT_OPERATOR_SUFFIX, then pulumi/<version>.
42+
// An explicitly empty value in either removes the key entirely, which opts out by restoring
43+
// upstream's `terraform/<version>` default.
44+
//
45+
// Note that the bridge skips PreConfigureCallback altogether when the provider config contains
46+
// unknowns, in which case no suffix is injected and upstream falls back to `terraform/<version>`.
47+
func setUserAgentOperatorSuffix(vars resource.PropertyMap) {
48+
if v, ok := vars[userAgentOperatorSuffixKey]; ok {
49+
if v.IsString() && v.StringValue() == "" {
50+
delete(vars, userAgentOperatorSuffixKey)
51+
}
52+
return
53+
}
54+
55+
if v, ok := os.LookupEnv(userAgentOperatorSuffixEnvVar); ok {
56+
if v == "" {
57+
return
58+
}
59+
vars[userAgentOperatorSuffixKey] = resource.NewStringProperty(v)
60+
return
61+
}
62+
63+
vars[userAgentOperatorSuffixKey] = resource.NewStringProperty(pulumiUserAgentOperatorSuffix())
64+
}

0 commit comments

Comments
 (0)