feat(ENG-13677): Terraform credential helper - #400
Conversation
- token returns expected owner/repo/token format - installer now takes --repo flag to set the reop - installer allows reinstalling/overwriting of repo
There was a problem hiding this comment.
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-cloudsmithlauncher into Terraform’s plugin directory and manages acredentials_helper "cloudsmith"block. - Adds CLI wiring for
cloudsmith credential-helper terraformand extendscredential-helper installto 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 nowrapper.pyundercloudsmith_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.
| # 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) |
| 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 |
| 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" |
There was a problem hiding this comment.
How does this respond on windows? I think window's path is %appdata%/terraform.d/plugins
| 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" |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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, ...]: |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
Description
Adds a Terraform credentials helper for Cloudsmith registries so
terraform initcan authenticate against a Cloudsmith Terraform registry withno 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 thatdiscovery contract, an installer that wires up
~/.terraformrc, and therepository-scoped token format the registry expects.
What's included
Command (
cloudsmith credential-helper terraform)getverb.Accepts Terraform's
[verb] <hostname>calling convention (verb defaults toget; hostname falls back to stdin) so the launcher can forward argsverbatim.
{"token": "..."}for a Cloudsmith host and{}for any other host(exit 0) so Terraform falls back to its own credential sources.
store/forgetand unknown verbs return an actionable error and a non-zeroexit. Missing credentials for a Cloudsmith host produce a clean refusal, never
a traceback.
credential-helpergroup.Installer (
credential-helper install/uninstall/list terraform)~/.terraform.d/pluginsbydefault;
--bin-diroverrides) and manages thecredentials_helper "cloudsmith"block in~/.terraformrcvia a regex block editor (no HCLparser); refuses if a different helper block already exists.
--org,-P/--profile, and--repointo the terraformrcargslist soterraform initneeds no environment variables. Computes theterraformrc change before writing the launcher so a conflict leaves no orphan.
Repository-scoped, custom-domain-aware token format
-r/--repo/--repository(orCLOUDSMITH_REPO) is required onget, andcan also be passed to
installto bake it into the terraformrcargs.*.cloudsmith.io/*.cloudsmith.comhosts →{org}/{repo}/{token}(the organisation is required here).
{repo}/{token}(the org is used only to resolve the domain, never emitted in the token).
is_standard_cloudsmith_domain()tocredential_helpers/common.pytodistinguish standard hosts from custom domains (
is_cloudsmith_domainwasrefactored to reuse it; behaviour unchanged).
Type of Change
Testing
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/--repobaking, conflict handling).
is_standard_cloudsmith_domainunit coverage (apex/subdomain/scheme/casingmatches, plus custom-domain and lookalike non-matches).
@pytest.mark.integration) that runs a realterraform initin an isolatedHOMEand asserts it does not fail forauthentication reasons; skips cleanly when
terraform, the wrapper, or thePYTEST_CLOUDSMITH_*vars are absent.Additional Notes
terraformrc block management, the required repo flag, and the token format).