Skip to content

Add user_agent_extra provider configuration attribute - #5863

Open
airizarryDB wants to merge 4 commits into
databricks:mainfrom
airizarryDB:add-user-agent-extra-provider-attribute
Open

Add user_agent_extra provider configuration attribute#5863
airizarryDB wants to merge 4 commits into
databricks:mainfrom
airizarryDB:add-user-agent-extra-provider-attribute

Conversation

@airizarryDB

@airizarryDB airizarryDB commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Changes

Adds a user_agent_extra attribute to the provider block, equivalent to the existing DATABRICKS_USER_AGENT_EXTRA environment variable introduced in #3520:

provider "databricks" {
  user_agent_extra = "databricks-sra/1.2.0"
}

Motivation

Terraform modules built on top of the provider (e.g. terraform-databricks-sra) want to ship usage attribution in their provider configuration. The environment variable works, but it must be set by whoever invokes Terraform — module authors can't set it on behalf of their users, so attribution silently disappears when users run terraform directly or via CI/CD, HCP Terraform, Spacelift, etc. A provider-block attribute travels with the module's provider.tf and requires no user action.

Implementation

  • The product-string parsing (ParseUserAgentExtra, RFC 9110 validation) moves unchanged from internal/providers/sdkv2 to internal/providers/common so the plugin framework provider can reuse it (pluginfw cannot import sdkv2), alongside a new ApplyUserAgentExtra helper shared by the env-var path and both configure paths.

  • The attribute is added to both muxed provider schemas (SDKv2 and plugin framework) to keep the mux schemas identical, and applied at configure time via useragent.WithUserAgentExtra.

  • The SDK deduplicates identical key/value pairs, so double application — from the two muxed providers being configured in the same process, or from the env var and the attribute both being set — is safe. Semantics (parsing, validation, error messages) are identical to the env var.

  • Per-configuration scoping (verified empirically): although useragent.WithUserAgentExtra is process-global, Terraform runs a separate plugin process per provider configuration and the plugin protocol sends a single Configure per process, so extras are effectively scoped to the provider configuration that set them. Verified with Terraform v1.14.0 against a local HTTP capture server using two aliased providers with different user_agent_extra values — every request carried exactly its own alias's product and never the other's:

    /api/2.0/preview/scim/v2/Me  databricks-tf-provider/1.121.0 ... module-one/1.1.1 sdk/sdkv2 data/yes resource/current_user auth/pat
    /api/2.0/preview/scim/v2/Me  databricks-tf-provider/1.121.0 ... module-two/2.2.2 sdk/sdkv2 data/yes resource/current_user auth/pat
    

    The only same-process double application is the mux fan-out of Configure to the SDKv2 and plugin framework servers (same value), which the SDK's exact key/value dedupe covers. In-process embeddings that configure multiple provider instances in one process (e.g. acceptance tests using provider factories) share the registry; a per-client mechanism would require SDK support (config.Config has no per-client user agent hook, and wrapping HTTPTransport would bypass the SDK's default transport and skip_verify handling).

  • Invalid product strings surface as a configure-time diagnostic instead of the env-var path's panic.

Tests

  • New test cases in sdkv2's TestConfigureDatabricksClient and pluginfw's TestConfigure verifying the attribute lands in the User-Agent, plus invalid-input tests asserting a configure-time error in both providers.

  • New TestMuxedProviderSchemaIncludesUserAgentExtra verifying the muxed provider serves a consistent schema including the new attribute.

  • Existing Test_ParseUserAgentExtra updated to the moved package; coverage unchanged.

  • make lint (staticcheck) and go test -short ./... pass locally (all 56 packages).

  • make test run locally

  • relevant change in docs/ folder

  • covered with integration tests in internal/acceptance

  • using Go SDK

  • using TF Plugin Framework

  • has entry in NEXT_CHANGELOG.md file

This pull request and its description were written by Isaac.

@airizarryDB
airizarryDB requested review from a team as code owners July 8, 2026 19:41
@airizarryDB
airizarryDB requested review from simonfaltum and removed request for a team July 8, 2026 19:41
@airizarryDB
airizarryDB temporarily deployed to test-trigger-is July 8, 2026 19:41 — with GitHub Actions Inactive
Adds a `user_agent_extra` attribute to the provider block, equivalent to
the existing DATABRICKS_USER_AGENT_EXTRA environment variable (databricks#3520).
This lets Terraform modules built on top of the provider (e.g.
terraform-databricks-sra) ship usage attribution in their provider
configuration without requiring users to set environment variables,
which are bypassed when terraform is invoked directly or via CI/CD.

The product-string parsing moves from internal/providers/sdkv2 to
internal/providers/common so the plugin framework provider can reuse it
(pluginfw cannot import sdkv2). The attribute is added to both muxed
provider schemas and applied at configure time via
useragent.WithUserAgentExtra, which deduplicates identical key/value
pairs, so double application from the two muxed providers or from
env var + attribute is safe.

Co-authored-by: Isaac
@airizarryDB
airizarryDB force-pushed the add-user-agent-extra-provider-attribute branch from 3686e17 to 98008f7 Compare July 10, 2026 14:23

@alexott alexott left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Found 1 issue:

  1. user_agent_extra is a provider configuration attribute, but both provider implementations apply it through useragent.WithUserAgentExtra, which the SDK documents as per-process state. If a Terraform run configures two Databricks provider instances or aliases with different user_agent_extra values, whichever provider configures first permanently registers its product for the whole plugin process, so requests from the other provider will also carry that module attribution. This makes the attribute behave differently from normal provider config and can misattribute requests across modules/workspaces. The existing context-based user-agent dimensions avoid this by adding data to the request context; this new value needs a provider/client-scoped path instead of the global registry.

if !userAgentExtra.IsNull() && !userAgentExtra.IsUnknown() {
if err := providercommon.ApplyUserAgentExtra(userAgentExtra.ValueString()); err != nil {
resp.Diagnostics.AddError("Failed to parse user_agent_extra", err.Error())
return nil
}

func ConfigureDatabricksClient(ctx context.Context, d *schema.ResourceData, configCustomizer func(*config.Config) error) (any, diag.Diagnostics) {
if v, ok := d.GetOk("user_agent_extra"); ok {
if err := providercommon.ApplyUserAgentExtra(v.(string)); err != nil {
return nil, diag.Errorf("failed to parse user_agent_extra: %s", err)
}
}

https://github.com/databricks/terraform-provider-databricks/blob/98008f782fc3f6dcf7fe254aac74d73d11bea09a/vendor/github.com/databricks/databricks-sdk-go/useragent/user_agent.go#L51-L55

Generated with Codex.

Terraform runs a separate plugin process per provider configuration and
sends a single Configure call per process, so configure-time extras are
effectively scoped to their provider configuration despite the registry
being process-global. Verified empirically with two aliased providers
against a local capture server: each alias's requests carry only its
own user_agent_extra products.

Co-authored-by: Isaac
@airizarryDB

Copy link
Copy Markdown
Contributor Author

Terraform runs a separate plugin process per provider configuration and sends a single Configure call per process, so configure-time extras are effectively scoped to their provider configuration despite the registry being process-global. Verified empirically with two aliased providers against a local capture server: each alias's requests carry only its own user_agent_extra products. @alexott

@airizarryDB

Copy link
Copy Markdown
Contributor Author

Re: per-process user-agent state concern

A code-review finding flagged that user_agent_extra is applied via useragent.WithUserAgentExtra, which the SDK documents as per-process global state, and raised the concern that two provider configurations with different user_agent_extra values could cross-contaminate each other's requests. I dug into this — the mechanism is correctly identified, but the failure scenario doesn't hold. Details and a reproducible harness below.

What's accurate: useragent.WithUserAgentExtra is genuinely per-process global state. There's no per-client alternative in the SDK — config.Config has no user-agent hook, and the header is stamped from global registry + request context at api_client.go:307.

What doesn't hold — the shared-process premise: I tested this empirically rather than reasoning from theory. I built the provider from this branch, dev-override'd it into Terraform v1.14.0, and ran a plan with two aliased providers (different user_agent_extra values) pointed at a local HTTP server that logs the User-Agent of every request.

Reproduction harness

server.py — captures the User-Agent of every request and returns a minimal SCIM Me response:

import http.server, json

class H(http.server.BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def do_GET(self):
        with open('ua.log', 'a') as f:
            f.write(self.path + "\t" + self.headers.get('User-Agent', '?') + "\n")
        body = json.dumps({"userName": "test@example.com"}).encode()
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        self.wfile.write(body)

http.server.HTTPServer(('127.0.0.1', 18080), H).serve_forever()

main.tf — two aliased providers, each with a distinct user_agent_extra:

terraform {
  required_providers {
    databricks = { source = "databricks/databricks" }
  }
}

provider "databricks" {
  alias            = "one"
  host             = "http://127.0.0.1:18080"
  token            = "fake-token-one"
  user_agent_extra = "module-one/1.1.1"
}

provider "databricks" {
  alias            = "two"
  host             = "http://127.0.0.1:18080"
  token            = "fake-token-two"
  user_agent_extra = "module-two/2.2.2"
}

data "databricks_current_user" "one" { provider = databricks.one }
data "databricks_current_user" "two" { provider = databricks.two }

tfrc — dev override pointing at the locally built provider binary:

provider_installation {
  dev_overrides {
    "databricks/databricks" = "/tmp/tfdev"
  }
  direct {}
}

Run:

go build -o /tmp/tfdev/terraform-provider-databricks .
python3 server.py &
TF_CLI_CONFIG_FILE=$PWD/tfrc TF_LOG=DEBUG terraform plan -input=false

Captured User-Agents — each alias's requests carry only its own product, never the other's:

/api/2.0/preview/scim/v2/Me   ... module-one/1.1.1 sdk/sdkv2 resource/current_user auth/pat
/api/2.0/preview/scim/v2/Me   ... module-two/2.2.2 sdk/sdkv2 resource/current_user auth/pat

Zero cross-contamination across the captured requests. The TF_LOG=DEBUG output shows why — Terraform spawns a separate provider process per configuration (distinct plugin started: ... pid= lines per provider), and the plugin protocol sends only one Configure per process, so "process-global" is de facto configuration-scoped. This matches existing in-tree precedent: WithUserAgentExtra("terraform", version) is already registered at configure time the same way.

Why not a client-scoped path: I evaluated it. The only post-header-stamp hook is wrapping cfg.HTTPTransport, but the SDK returns a custom transport unconditionally when that field is set — a wrapper would silently bypass the SDK's tuned default transport and break the skip_verify provider option. A proper fix needs an SDK change (a per-client user-agent field on config.Config). Worth filing as a follow-up, but it shouldn't gate this PR.

Residual caveat (real but narrow): in-process embeddings that configure multiple provider instances in one process — e.g. acceptance tests using provider factories — do share the registry. Same value → deduped by the SDK; different values → both appear.

Changes made in response: documented the scoping guarantee and the caveat in the ApplyUserAgentExtra doc comment (6cecc82), and replaced the earlier PR-body claim with this experiment evidence so reviewers hitting the same concern have the receipts.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Unit tests

If this PR is from a fork, the tests check runs offline against a pre-warmed Go module cache because fork PRs cannot authenticate to the internal Go module proxy.

If this PR changes go.mod or go.sum, the tests check will fail until a maintainer warms the cache for it:

Actions -> Warm Go Cache -> Run workflow -> pr_number = 5863

Re-run the failed check once the cache warming completes.

Integration tests

Integration tests don't run automatically for external contributors; an authorized user can run them manually by following the instructions below:

Trigger:
go/deco-tests-run/terraform

Inputs:

  • PR number: 5863
  • Commit SHA: f0a71729b9e0919c9fc91997fedc1a08be24ae79

Checks will be approved automatically on success.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants