Skip to content

feat(ENG-13677): Terraform credential helper - #400

Open
tigh-latte wants to merge 11 commits into
masterfrom
tigh/terraform-credential-provider
Open

feat(ENG-13677): Terraform credential helper#400
tigh-latte wants to merge 11 commits into
masterfrom
tigh/terraform-credential-provider

Conversation

@tigh-latte

@tigh-latte tigh-latte commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a Terraform credentials helper for Cloudsmith registries so
terraform init can authenticate against a Cloudsmith Terraform registry with
no token on disk, reusing the existing Cloudsmith CLI credential chain
(--api-key/CLOUDSMITH_API_KEY, credentials.ini, the OS keyring, or OIDC).

Terraform discovers credentials helpers as executables named
terraform-credentials-<name> and only searches its plugin directories (never
$PATH). This PR ships the helper, a launcher/wrapper that satisfies that
discovery contract, an installer that wires up ~/.terraformrc, and the
repository-scoped token format the registry expects.

What's included

Command (cloudsmith credential-helper terraform)

  • Implements Terraform's credentials-helper protocol for the get verb.
    Accepts Terraform's [verb] <hostname> calling convention (verb defaults to
    get; hostname falls back to stdin) so the launcher can forward args
    verbatim.
  • Returns {"token": "..."} for a Cloudsmith host and {} for any other host
    (exit 0) so Terraform falls back to its own credential sources.
    store/forget and unknown verbs return an actionable error and a non-zero
    exit. Missing credentials for a Cloudsmith host produce a clean refusal, never
    a traceback.
  • Registered on the credential-helper group.

Installer (credential-helper install/uninstall/list terraform)

  • Writes the launcher into Terraform's plugin dir (~/.terraform.d/plugins by
    default; --bin-dir overrides) and manages the credentials_helper "cloudsmith" block in ~/.terraformrc via a regex block editor (no HCL
    parser); refuses if a different helper block already exists.
  • Bakes the resolved --org, -P/--profile, and --repo into the terraformrc
    args list so terraform init needs no environment variables. Computes the
    terraformrc change before writing the launcher so a conflict leaves no orphan.

Repository-scoped, custom-domain-aware token format

  • Terraform never tells a credentials helper which repository is requested, so
    -r/--repo/--repository (or CLOUDSMITH_REPO) is required on get, and
    can also be passed to install to bake it into the terraformrc args.
  • Tokens are scoped per repository:
    • Standard *.cloudsmith.io/*.cloudsmith.com hosts → {org}/{repo}/{token}
      (the organisation is required here).
    • Custom domains (already bound to a single organisation) → {repo}/{token}
      (the org is used only to resolve the domain, never emitted in the token).
  • Added is_standard_cloudsmith_domain() to credential_helpers/common.py to
    distinguish standard hosts from custom domains (is_cloudsmith_domain was
    refactored to reuse it; behaviour unchanged).

Type of Change

  • New feature

Testing

  • Unit tests for the runtime (get/store/forget, token / empty-object /
    refusal outcomes, org-required-on-standard-domain, org-omitted-on-custom-
    domain), the CLI shim, the wrapper delegation, the terraformrc block
    helpers, and the installer (launcher + terraformrc, --org/-P/--repo
    baking, conflict handling).
  • is_standard_cloudsmith_domain unit coverage (apex/subdomain/scheme/casing
    matches, plus custom-domain and lookalike non-matches).
  • A live integration test (@pytest.mark.integration) that runs a real
    terraform init in an isolated HOME and asserts it does not fail for
    authentication reasons; skips cleanly when terraform, the wrapper, or the
    PYTEST_CLOUDSMITH_* vars are absent.
env -u CLOUDSMITH_PROFILE -u CLOUDSMITH_ORG -u CLOUDSMITH_WORKSPACE \
    -u CLOUDSMITH_KEYRING_BACKEND HOME=$(mktemp -d) \
    pytest -m "not integration"

Additional Notes

Copilot AI lite review requested due to automatic review settings August 31, 2026 08:54
@tigh-latte
tigh-latte requested a review from a team as a code owner August 31, 2026 08:54

Copilot AI 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.

Pull request overview

Adds a Terraform credentials helper integration to Cloudsmith CLI so terraform init can authenticate against Cloudsmith Terraform registries via the existing CLI credential chain (env/config/keyring/OIDC), plus installer logic to wire Terraform’s plugin dir and ~/.terraformrc.

Changes:

  • Introduces Terraform helper runtime + terraformrc block editor + installer that writes a terraform-credentials-cloudsmith launcher into Terraform’s plugin directory and manages a credentials_helper "cloudsmith" block.
  • Adds CLI wiring for cloudsmith credential-helper terraform and extends credential-helper install to support Terraform-specific baked args (org/profile/repo) and next-steps guidance.
  • Adds unit + integration tests and refactors domain detection by introducing is_standard_cloudsmith_domain().

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cloudsmith_cli/credential_helpers/terraform/terraformrc.py Text-based add/update/remove of the credentials_helper "cloudsmith" terraformrc block.
cloudsmith_cli/credential_helpers/terraform/runtime.py Implements Terraform credentials-helper protocol behavior and scoped token formatting.
cloudsmith_cli/credential_helpers/terraform/installer.py Installs/removes the launcher into Terraform plugin dir and updates terraformrc.
cloudsmith_cli/credential_helpers/terraform/init.py Exposes Terraform helper runtime functions at package level.
cloudsmith_cli/credential_helpers/common.py Adds is_standard_cloudsmith_domain() and reuses it in is_cloudsmith_domain().
cloudsmith_cli/cli/commands/credential_helper/terraform.py Click command shim for Terraform helper (parses verb/hostname, prints JSON).
cloudsmith_cli/cli/commands/credential_helper/manage.py Registers Terraform installer; adds Terraform repo bake + next-steps output.
cloudsmith_cli/cli/commands/credential_helper/init.py Registers the terraform subcommand under credential-helper.
cloudsmith_cli/cli/tests/test_startup_imports.py Adds import-safety tests for Terraform helper (and wrapper).
cloudsmith_cli/cli/tests/test_credential_helper_terraform.py Adds runtime/CLI/wrapper behavior tests for Terraform helper.
cloudsmith_cli/cli/tests/test_credential_helper_terraform_installer.py Adds terraformrc + installer + CLI install/uninstall tests.
cloudsmith_cli/cli/tests/commands/test_credential_helper.py Adds unit coverage for is_standard_cloudsmith_domain().
cloudsmith_cli/cli/tests/commands/test_credential_helper_terraform_integration.py Adds live terraform init integration test exercising helper auth path.
CHANGELOG.md Documents the new Terraform credentials helper feature.
Suppressed comments (1)

cloudsmith_cli/cli/tests/test_startup_imports.py:76

  • This test imports cloudsmith_cli.credential_helpers.terraform.wrapper, but there is no wrapper.py under cloudsmith_cli/credential_helpers/terraform/ in this PR (only __init__.py, runtime.py, installer.py, terraformrc.py). This will fail at import time and break the suite.

Either add the missing wrapper module (and its packaging entry point if it’s meant to be an installed executable), or adjust/remove the wrapper-related tests if the launcher written by TerraformInstaller is the only wrapper being shipped.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +126 to +133
# Scope the token to a single repository. Standard domains also carry the
# org ("{org}/{repo}/{token}"); custom domains are already org-bound, so
# they omit it ("{repo}/{token}").
if standard_domain:
scoped_token = f"{org}/{repo}/{token}"
else:
scoped_token = f"{repo}/{token}"
return (0, json.dumps({"token": scoped_token}), None)
Comment on lines +102 to +107
if match.group("name") != HELPER_NAME:
raise TerraformrcConflictError(match.group("name"))

# Replace the existing Cloudsmith block in place.
new_text = text[: match.start()] + block + text[match.end() :]
return new_text, new_text != text
Comment on lines +25 to +33
from ...core.credentials.models import CredentialResult
from ...credential_helpers.backends import BackendKind
from ...credential_helpers.terraform import wrapper
from ...credential_helpers.terraform.runtime import (
_MISSING_ORG_MESSAGE,
_REFUSAL_MESSAGE,
execute,
get_token,
)
``~/.terraform.d/plugins`` on all platforms — the best-known of Terraform's
default credentials-helper search locations and writable without elevation.
"""
return Path.home() / ".terraform.d" / "plugins"

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.

def _launcher_path(self, target_dir: Path) -> Path:
"""Return the launcher path within *target_dir* for this platform."""
if os.name == "nt":
return target_dir / f"{self.LAUNCHER_NAME}.cmd"

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.

Will .cmd successfully launch on windows as a plugin? https://github.com/hashicorp/terraform/blob/main/internal/providercache/cached_provider.go#L106 looks like the discovery process will get it. Might be worth spinning up a windows instance on aws and testing.

return f'"{sys.executable}" credential-helper terraform'
return cls.TARGET_CMD

def _resolve_bin_dir(self, bin_dir: str | None) -> Path:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Terraform searches only its default plugin locations. Installation can therefore succeed while Terraform cannot find the helper. Could this be restricted to recognized Terraform plugin directories?

return cls()


def _terraform_helper_args(ctx, opts, repo: str | None = None) -> tuple[str, ...]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How does this work with --config-file / --credentials-file flags? If an installation uses either option, terraform init may not resolve the same credentials (?)

except (OSError, ValueError):
hostname = ""

exit_code, stdout, stderr = execute(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The helper reaches execute() without reading stdin for store. Terraform requires unsupported store operations to consume the complete payload before returning an error. Could stdin be drained first?

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants