cloudsmith-cli 1.26.0__tar.gz → 1.27.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudsmith_cli-1.26.0/cloudsmith_cli.egg-info → cloudsmith_cli-1.27.0}/PKG-INFO +7 -4
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/README.md +2 -2
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/auth.py +129 -34
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/credential_helper/__init__.py +2 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/credential_helper/manage.py +127 -10
- cloudsmith_cli-1.27.0/cloudsmith_cli/cli/commands/credential_helper/terraform.py +151 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/decorators.py +67 -11
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/saml.py +2 -2
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/webserver.py +1 -3
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/models.py +6 -0
- cloudsmith_cli-1.27.0/cloudsmith_cli/core/credentials/providers/keyring_provider.py +95 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/providers/oidc_provider.py +81 -5
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/keyring.py +31 -7
- cloudsmith_cli-1.27.0/cloudsmith_cli/core/sso.py +224 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/utils.py +50 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/common.py +23 -3
- cloudsmith_cli-1.27.0/cloudsmith_cli/credential_helpers/terraform/__init__.py +4 -0
- cloudsmith_cli-1.27.0/cloudsmith_cli/credential_helpers/terraform/installer.py +496 -0
- cloudsmith_cli-1.27.0/cloudsmith_cli/credential_helpers/terraform/runtime.py +204 -0
- cloudsmith_cli-1.27.0/cloudsmith_cli/credential_helpers/terraform/terraformrc.py +165 -0
- cloudsmith_cli-1.27.0/cloudsmith_cli/data/VERSION +1 -0
- cloudsmith_cli-1.27.0/cloudsmith_cli/wrapper.py +48 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0/cloudsmith_cli.egg-info}/PKG-INFO +7 -4
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli.egg-info/SOURCES.txt +6 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli.egg-info/entry_points.txt +1 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli.egg-info/requires.txt +4 -1
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/pyproject.toml +8 -1
- cloudsmith_cli-1.26.0/cloudsmith_cli/core/credentials/providers/keyring_provider.py +0 -105
- cloudsmith_cli-1.26.0/cloudsmith_cli/core/sso.py +0 -49
- cloudsmith_cli-1.26.0/cloudsmith_cli/data/VERSION +0 -1
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/LICENSE +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/MANIFEST.in +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/__main__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/command.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/check.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/copy.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/credential_helper/cargo.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/credential_helper/docker.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/credential_helper/generic.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/credential_helper/pnpm.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/delete.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/dependencies.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/docs.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/domains.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/download.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/entitlements.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/help_.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/list_.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/login.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/logout.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/main.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/mcp.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/metadata.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/metrics/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/metrics/command.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/metrics/entitlements.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/metrics/packages.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/move.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/policy/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/policy/command.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/policy/deny.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/policy/license.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/policy/vulnerability.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/push.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/quarantine.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/quota/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/quota/command.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/quota/history.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/quota/quota.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/registry.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/repos.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/resync.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/status.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/tags.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/tokens.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/upstream.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/vulnerabilities.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/commands/whoami.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/config.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/dsc_parser.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/exceptions.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/metadata_common.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/table.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/types.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/utils.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/cli/validators.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/conftest.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/distros.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/entitlements.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/exceptions.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/files.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/init.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/metadata.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/metrics.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/orgs.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/packages.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/quota.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/rates.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/repos.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/status.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/upstreams.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/user.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/version.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/api/vulnerabilities.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/cache_utils.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/config.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/chain.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/cache.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/aws.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/base.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/generic.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/oidc/exchange.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/provider.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/providers/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/providers/cli_flag.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/providers/credentials_file.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/credentials/providers/env_var.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/download.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/macos_keychain.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/mcp/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/mcp/data.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/mcp/server.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/pagination.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/ratelimits.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/rest.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/session.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/core/version.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/backends.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/cargo/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/cargo/installer.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/cargo/runtime.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/custom_domains.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/default_domains.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/docker/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/docker/installer.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/docker/runtime.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/generic.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/launchers.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/pnpm/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/pnpm/installer.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/pnpm/rc.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/credential_helpers/pnpm/runtime.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/data/config.ini +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/data/credentials.ini +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/templates/__init__.py +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/templates/auth_error.html +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/templates/auth_success.html +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli/templates/auth_two_factor.html +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli.egg-info/dependency_links.txt +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/cloudsmith_cli.egg-info/top_level.txt +0 -0
- {cloudsmith_cli-1.26.0 → cloudsmith_cli-1.27.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudsmith-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.27.0
|
|
4
4
|
Summary: Cloudsmith Command-Line Interface (CLI)
|
|
5
5
|
Author-email: Cloudsmith Ltd <support@cloudsmith.io>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -32,13 +32,16 @@ Requires-Dist: cloudsmith-api<3.0,>=2.0.31
|
|
|
32
32
|
Requires-Dist: keyring>=25.4.1
|
|
33
33
|
Requires-Dist: keyrings-alt>=5.0.2
|
|
34
34
|
Requires-Dist: keyrings-cryptfile>=1.3.9
|
|
35
|
+
Requires-Dist: cryptography>=50.0.0
|
|
35
36
|
Requires-Dist: mcp==1.28.1
|
|
36
|
-
Requires-Dist:
|
|
37
|
+
Requires-Dist: pydantic-settings>=2.14.2
|
|
38
|
+
Requires-Dist: PyJWT[crypto]>=2.13.0
|
|
37
39
|
Requires-Dist: python-toon==0.1.2
|
|
38
40
|
Requires-Dist: requests>=2.18.4
|
|
39
41
|
Requires-Dist: requests_toolbelt>=1.0.0
|
|
40
42
|
Requires-Dist: rich>=13.0.0
|
|
41
43
|
Requires-Dist: semver>=2.7.9
|
|
44
|
+
Requires-Dist: starlette>=1.3.1
|
|
42
45
|
Requires-Dist: urllib3>=2.5
|
|
43
46
|
Requires-Dist: tomlkit>=0.15.0
|
|
44
47
|
Provides-Extra: aws
|
|
@@ -370,7 +373,7 @@ cloudsmith auth --owner example
|
|
|
370
373
|
Beginning authentication for the example org ...
|
|
371
374
|
Your organization's SAML IDP URL is: https://example.com/some-saml-idp
|
|
372
375
|
|
|
373
|
-
|
|
376
|
+
Waiting for the authentication callback on port 12400 ...
|
|
374
377
|
|
|
375
378
|
Authentication complete
|
|
376
379
|
```
|
|
@@ -381,7 +384,7 @@ The CLI opens the IDP URL in your browser automatically. If it can't (for exampl
|
|
|
381
384
|
cloudsmith auth --owner example --no-browser
|
|
382
385
|
```
|
|
383
386
|
|
|
384
|
-
*Note:* The authentication callback is served on `127.0.0.1
|
|
387
|
+
*Note:* The authentication callback is served on `127.0.0.1`, on the first free port between `12400` and `12404`. The browser you open the URL in must run on the same machine as the CLI.
|
|
385
388
|
|
|
386
389
|
#### Getting Your API Key
|
|
387
390
|
|
|
@@ -321,7 +321,7 @@ cloudsmith auth --owner example
|
|
|
321
321
|
Beginning authentication for the example org ...
|
|
322
322
|
Your organization's SAML IDP URL is: https://example.com/some-saml-idp
|
|
323
323
|
|
|
324
|
-
|
|
324
|
+
Waiting for the authentication callback on port 12400 ...
|
|
325
325
|
|
|
326
326
|
Authentication complete
|
|
327
327
|
```
|
|
@@ -332,7 +332,7 @@ The CLI opens the IDP URL in your browser automatically. If it can't (for exampl
|
|
|
332
332
|
cloudsmith auth --owner example --no-browser
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
-
*Note:* The authentication callback is served on `127.0.0.1
|
|
335
|
+
*Note:* The authentication callback is served on `127.0.0.1`, on the first free port between `12400` and `12404`. The browser you open the URL in must run on the same machine as the CLI.
|
|
336
336
|
|
|
337
337
|
#### Getting Your API Key
|
|
338
338
|
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
import webbrowser
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
|
+
import requests
|
|
6
7
|
|
|
8
|
+
from ...core.sso import (
|
|
9
|
+
SsoRenewalStatus,
|
|
10
|
+
get_access_token_expiry,
|
|
11
|
+
renew_sso_session,
|
|
12
|
+
)
|
|
7
13
|
from .. import decorators, utils, validators
|
|
8
14
|
from ..exceptions import handle_api_exceptions
|
|
9
15
|
from ..saml import create_configured_session, get_idp_url
|
|
@@ -13,7 +19,86 @@ from .tokens import create, request_api_key
|
|
|
13
19
|
|
|
14
20
|
# Authentication server configuration
|
|
15
21
|
AUTH_SERVER_HOST = "127.0.0.1"
|
|
16
|
-
|
|
22
|
+
AUTH_REDIRECT_HOST = "localhost"
|
|
23
|
+
AUTH_SERVER_PORTS = (12400, 12401, 12402, 12403, 12404)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _create_auth_server(opts, owner, session, enable_token_creation, profile):
|
|
27
|
+
"""Create an authentication server on the first available callback port."""
|
|
28
|
+
last_error = None
|
|
29
|
+
|
|
30
|
+
for port in AUTH_SERVER_PORTS:
|
|
31
|
+
try:
|
|
32
|
+
server = AuthenticationWebServer(
|
|
33
|
+
(AUTH_SERVER_HOST, port),
|
|
34
|
+
AuthenticationWebRequestHandler,
|
|
35
|
+
owner=owner,
|
|
36
|
+
session=session,
|
|
37
|
+
debug=opts.debug,
|
|
38
|
+
refresh_api_on_success=enable_token_creation,
|
|
39
|
+
api_opts=opts.api_config,
|
|
40
|
+
profile=profile,
|
|
41
|
+
)
|
|
42
|
+
return server, port
|
|
43
|
+
except OSError as exc:
|
|
44
|
+
last_error = exc
|
|
45
|
+
|
|
46
|
+
ports = ", ".join(str(port) for port in AUTH_SERVER_PORTS)
|
|
47
|
+
raise click.ClickException(
|
|
48
|
+
"Could not start the authentication callback server. "
|
|
49
|
+
f"Every candidate port is unavailable: {ports}. "
|
|
50
|
+
f"The last error was: {last_error}. "
|
|
51
|
+
"Stop the process that holds one of these ports, then try again."
|
|
52
|
+
) from last_error
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _renew_existing_sso_session(opts, profile):
|
|
56
|
+
"""Return a usable existing SSO session, or fall back to browser auth."""
|
|
57
|
+
renewal = renew_sso_session(opts.api_config.host, opts.session, profile=profile)
|
|
58
|
+
if renewal.status in (
|
|
59
|
+
SsoRenewalStatus.RENEWED,
|
|
60
|
+
SsoRenewalStatus.CURRENT,
|
|
61
|
+
):
|
|
62
|
+
return renewal
|
|
63
|
+
if renewal.status == SsoRenewalStatus.FAILED and isinstance(
|
|
64
|
+
renewal.error, requests.RequestException
|
|
65
|
+
):
|
|
66
|
+
raise click.ClickException(
|
|
67
|
+
"The SSO session has expired and Cloudsmith could not be reached. "
|
|
68
|
+
"Check your connection, then run 'cloudsmith auth' again."
|
|
69
|
+
) from renewal.error
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _report_active_sso_session(opts, renewal, use_stderr=False):
|
|
74
|
+
"""Report the usable SSO session without exposing its access token."""
|
|
75
|
+
expires_at = get_access_token_expiry(renewal.access_token)
|
|
76
|
+
data = {
|
|
77
|
+
"authenticated": True,
|
|
78
|
+
"method": "sso",
|
|
79
|
+
"status": renewal.status.value,
|
|
80
|
+
"expires_at": expires_at,
|
|
81
|
+
"renewal_error": str(renewal.error) if renewal.error else None,
|
|
82
|
+
}
|
|
83
|
+
if utils.maybe_print_as_json(opts, data):
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
if renewal.status == SsoRenewalStatus.RENEWED:
|
|
87
|
+
click.secho("SSO session renewed.", fg="green", err=use_stderr)
|
|
88
|
+
elif renewal.error:
|
|
89
|
+
click.secho(
|
|
90
|
+
"The SSO session could not be renewed; the existing access token "
|
|
91
|
+
"remains active.",
|
|
92
|
+
fg="yellow",
|
|
93
|
+
err=use_stderr,
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
click.secho("SSO session is active.", fg="green", err=use_stderr)
|
|
97
|
+
if expires_at:
|
|
98
|
+
click.echo(
|
|
99
|
+
f"Access token expires at {utils.fmt_datetime(expires_at)}.",
|
|
100
|
+
err=use_stderr,
|
|
101
|
+
)
|
|
17
102
|
|
|
18
103
|
|
|
19
104
|
def _perform_saml_authentication(
|
|
@@ -28,50 +113,52 @@ def _perform_saml_authentication(
|
|
|
28
113
|
session = create_configured_session(opts)
|
|
29
114
|
api_host = opts.api_config.host
|
|
30
115
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
click.echo(
|
|
34
|
-
f"Your Workspace's SAML IDP URL is: {click.style(idp_url, bold=True)}",
|
|
35
|
-
err=use_stderr,
|
|
116
|
+
auth_server, port = _create_auth_server(
|
|
117
|
+
opts, owner, session, enable_token_creation, profile
|
|
36
118
|
)
|
|
37
|
-
|
|
119
|
+
redirect_url = f"http://{AUTH_REDIRECT_HOST}:{port}"
|
|
38
120
|
|
|
39
|
-
|
|
121
|
+
try:
|
|
40
122
|
click.echo(
|
|
41
|
-
"
|
|
42
|
-
"Please open the URL above manually to continue.",
|
|
123
|
+
f"Waiting for the authentication callback on port {port} ... ",
|
|
43
124
|
err=use_stderr,
|
|
44
125
|
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
126
|
+
|
|
127
|
+
idp_url = get_idp_url(
|
|
128
|
+
api_host, owner, redirect_url=redirect_url, session=session
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
click.echo(
|
|
132
|
+
f"Your Workspace's SAML IDP URL is: {click.style(idp_url, bold=True)}",
|
|
133
|
+
err=use_stderr,
|
|
134
|
+
)
|
|
135
|
+
click.echo(err=use_stderr)
|
|
136
|
+
|
|
137
|
+
if no_browser:
|
|
55
138
|
click.echo(
|
|
56
|
-
"
|
|
139
|
+
"Skipping automatic browser launch (--no-browser). "
|
|
57
140
|
"Please open the URL above manually to continue.",
|
|
58
141
|
err=use_stderr,
|
|
59
142
|
)
|
|
143
|
+
else:
|
|
144
|
+
try:
|
|
145
|
+
browser_opened = webbrowser.open(idp_url)
|
|
146
|
+
except Exception:
|
|
147
|
+
# Browser launch failures vary by platform (webbrowser.Error on
|
|
148
|
+
# Cygwin/headless, anything else elsewhere), so catch broadly and
|
|
149
|
+
# fall back to the manual URL rather than crashing.
|
|
150
|
+
browser_opened = False
|
|
60
151
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
session=session,
|
|
68
|
-
debug=opts.debug,
|
|
69
|
-
refresh_api_on_success=enable_token_creation,
|
|
70
|
-
api_opts=opts.api_config,
|
|
71
|
-
profile=profile,
|
|
72
|
-
)
|
|
152
|
+
if not browser_opened:
|
|
153
|
+
click.echo(
|
|
154
|
+
"Couldn't open a browser automatically. "
|
|
155
|
+
"Please open the URL above manually to continue.",
|
|
156
|
+
err=use_stderr,
|
|
157
|
+
)
|
|
73
158
|
|
|
74
|
-
|
|
159
|
+
auth_server.handle_request()
|
|
160
|
+
finally:
|
|
161
|
+
auth_server.server_close()
|
|
75
162
|
|
|
76
163
|
|
|
77
164
|
@main.command(aliases=["auth"])
|
|
@@ -165,6 +252,14 @@ def authenticate(
|
|
|
165
252
|
err=True,
|
|
166
253
|
)
|
|
167
254
|
|
|
255
|
+
if not force and not token and not request_api_key_flag:
|
|
256
|
+
session_result = _renew_existing_sso_session(
|
|
257
|
+
opts, profile=ctx.meta.get("profile")
|
|
258
|
+
)
|
|
259
|
+
if session_result:
|
|
260
|
+
_report_active_sso_session(opts, session_result, use_stderr)
|
|
261
|
+
return
|
|
262
|
+
|
|
168
263
|
workspace = opts.org or click.prompt("Workspace", err=use_stderr)
|
|
169
264
|
workspace = validators.validate_owner(ctx, None, workspace)[0]
|
|
170
265
|
opts.org = workspace
|
|
@@ -14,6 +14,7 @@ from .docker import docker as docker_cmd
|
|
|
14
14
|
from .generic import generic as generic_cmd
|
|
15
15
|
from .manage import install_cmd, list_cmd, uninstall_cmd
|
|
16
16
|
from .pnpm import pnpm as pnpm_cmd
|
|
17
|
+
from .terraform import terraform as terraform_cmd
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@click.group()
|
|
@@ -50,5 +51,6 @@ credential_helper.add_command(install_cmd, name="install")
|
|
|
50
51
|
credential_helper.add_command(uninstall_cmd, name="uninstall")
|
|
51
52
|
credential_helper.add_command(list_cmd, name="list")
|
|
52
53
|
credential_helper.add_command(cargo_cmd, name="cargo")
|
|
54
|
+
credential_helper.add_command(terraform_cmd, name="terraform")
|
|
53
55
|
|
|
54
56
|
main.add_command(credential_helper, name="credential-helper")
|
|
@@ -15,6 +15,15 @@ import click
|
|
|
15
15
|
from cloudsmith_cli.credential_helpers.cargo.installer import CargoInstaller
|
|
16
16
|
from cloudsmith_cli.credential_helpers.generic import PartialInstallError
|
|
17
17
|
from cloudsmith_cli.credential_helpers.pnpm.installer import PNPMInstaller
|
|
18
|
+
from cloudsmith_cli.credential_helpers.terraform.installer import (
|
|
19
|
+
TerraformHelperExeNotFound,
|
|
20
|
+
TerraformInstaller,
|
|
21
|
+
TerraformPluginDirError,
|
|
22
|
+
_terraformrc_path,
|
|
23
|
+
)
|
|
24
|
+
from cloudsmith_cli.credential_helpers.terraform.terraformrc import (
|
|
25
|
+
TerraformrcConflictError,
|
|
26
|
+
)
|
|
18
27
|
|
|
19
28
|
from ....credential_helpers.docker.installer import DockerInstaller
|
|
20
29
|
from ... import utils
|
|
@@ -33,6 +42,7 @@ _INSTALLERS: dict[str, type] = {
|
|
|
33
42
|
"docker": DockerInstaller,
|
|
34
43
|
"pnpm": PNPMInstaller,
|
|
35
44
|
"cargo": CargoInstaller,
|
|
45
|
+
"terraform": TerraformInstaller,
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
|
|
@@ -65,6 +75,72 @@ def _get_installer(name: str):
|
|
|
65
75
|
return cls()
|
|
66
76
|
|
|
67
77
|
|
|
78
|
+
def _terraform_helper_args(ctx, opts, repo: str | None = None) -> tuple[str, ...]:
|
|
79
|
+
"""Build the terraformrc ``args`` list from the resolved config.
|
|
80
|
+
|
|
81
|
+
Baking ``--org``/``-r``/``-P`` into the block means ``terraform init`` works
|
|
82
|
+
with no environment variables and no hand-edited config. Only values
|
|
83
|
+
actually supplied are written, so an install without ``--org``/``--repo``/
|
|
84
|
+
``-P`` leaves ``args = []``.
|
|
85
|
+
|
|
86
|
+
A non-default ``--config-file``/``--credentials-file`` is also baked in: the
|
|
87
|
+
helper is later invoked by ``terraform init`` in a fresh process with none
|
|
88
|
+
of this invocation's flags, so without pinning them Terraform would resolve
|
|
89
|
+
credentials from the default search path instead of the files the operator
|
|
90
|
+
installed with.
|
|
91
|
+
"""
|
|
92
|
+
args: list[str] = []
|
|
93
|
+
if opts.org:
|
|
94
|
+
args.extend(["--org", opts.org])
|
|
95
|
+
if repo:
|
|
96
|
+
args.extend(["-r", repo])
|
|
97
|
+
profile = ctx.meta.get("profile")
|
|
98
|
+
if profile:
|
|
99
|
+
args.extend(["-P", profile])
|
|
100
|
+
config_file = ctx.meta.get("config_file")
|
|
101
|
+
if config_file:
|
|
102
|
+
args.extend(["--config-file", config_file])
|
|
103
|
+
creds_file = ctx.meta.get("creds_file")
|
|
104
|
+
if creds_file:
|
|
105
|
+
args.extend(["--credentials-file", creds_file])
|
|
106
|
+
return tuple(args)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_REPO_FLAGS = ("-r", "--repo", "--repository")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _terraform_next_steps(helper_args: tuple[str, ...]) -> list[str]:
|
|
113
|
+
"""Return the post-install guidance for the required repository.
|
|
114
|
+
|
|
115
|
+
Terraform never tells the credentials helper which repository is being
|
|
116
|
+
requested, so the helper needs the repository supplied out-of-band. If it
|
|
117
|
+
was not baked into the generated ``args`` at install time, tell the user how
|
|
118
|
+
to provide it: set ``CLOUDSMITH_REPO`` or add ``--repo`` to the terraformrc
|
|
119
|
+
``args``. Returns an empty list when a repository is already configured.
|
|
120
|
+
"""
|
|
121
|
+
has_repo = any(
|
|
122
|
+
a in _REPO_FLAGS or a.split("=", 1)[0] in _REPO_FLAGS for a in helper_args
|
|
123
|
+
)
|
|
124
|
+
if has_repo:
|
|
125
|
+
return []
|
|
126
|
+
rc_path = _terraformrc_path()
|
|
127
|
+
return [
|
|
128
|
+
(
|
|
129
|
+
"Next steps: the Terraform helper requires a repository, which"
|
|
130
|
+
" Terraform does not pass to credentials helpers. Provide it in one"
|
|
131
|
+
" of these ways:"
|
|
132
|
+
),
|
|
133
|
+
(
|
|
134
|
+
" - export CLOUDSMITH_REPO=<your-repo> in the environment that runs"
|
|
135
|
+
" `terraform init`, or"
|
|
136
|
+
),
|
|
137
|
+
(
|
|
138
|
+
f" - add it to the args in {rc_path}, e.g."
|
|
139
|
+
' args = ["--repo", "<your-repo>", ...].'
|
|
140
|
+
),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
68
144
|
# ---------------------------------------------------------------------------
|
|
69
145
|
# install
|
|
70
146
|
# ---------------------------------------------------------------------------
|
|
@@ -99,6 +175,16 @@ def _get_installer(name: str):
|
|
|
99
175
|
default=False,
|
|
100
176
|
help="Bypass the custom-domain cache and fetch fresh data from the API.",
|
|
101
177
|
)
|
|
178
|
+
@click.option(
|
|
179
|
+
"-r",
|
|
180
|
+
"--repo",
|
|
181
|
+
"--repository",
|
|
182
|
+
"repo",
|
|
183
|
+
default=None,
|
|
184
|
+
help="Terraform only: bake this repository into the terraformrc `args` "
|
|
185
|
+
"list (as `-r <repo>`) so `terraform init` needs no CLOUDSMITH_REPO env "
|
|
186
|
+
"var. Terraform does not pass the repository to a credentials helper.",
|
|
187
|
+
)
|
|
102
188
|
@common_cli_config_options
|
|
103
189
|
@common_cli_output_options
|
|
104
190
|
@common_api_auth_options
|
|
@@ -113,6 +199,7 @@ def install_cmd(
|
|
|
113
199
|
dry_run: bool,
|
|
114
200
|
no_discover: bool,
|
|
115
201
|
refresh: bool,
|
|
202
|
+
repo: str | None,
|
|
116
203
|
) -> None:
|
|
117
204
|
"""Install a credential helper launcher and configure the package manager.
|
|
118
205
|
|
|
@@ -140,19 +227,41 @@ def install_cmd(
|
|
|
140
227
|
\b
|
|
141
228
|
# Disable automatic custom-domain discovery
|
|
142
229
|
$ cloudsmith credential-helper install HELPER --no-discover
|
|
230
|
+
|
|
231
|
+
\b
|
|
232
|
+
# Terraform: bake the repository into the terraformrc args so
|
|
233
|
+
# `terraform init` needs no CLOUDSMITH_REPO env var
|
|
234
|
+
$ cloudsmith credential-helper install terraform --org acme --repo my-repo
|
|
143
235
|
"""
|
|
144
236
|
installer = _get_installer(helper)
|
|
237
|
+
|
|
238
|
+
install_kwargs = {
|
|
239
|
+
"bin_dir": bin_dir,
|
|
240
|
+
"domains": domains,
|
|
241
|
+
"dry_run": dry_run,
|
|
242
|
+
"discover": not no_discover,
|
|
243
|
+
"refresh": refresh,
|
|
244
|
+
"org": opts.org,
|
|
245
|
+
"credential": opts.credential,
|
|
246
|
+
"api_host": opts.api_host,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Terraform bakes the resolved org/profile into the terraformrc `args` list
|
|
250
|
+
# so `terraform init` needs neither env vars nor a hand-edited config. The
|
|
251
|
+
# wrapper forwards these to the CLI ahead of the hostname at call time.
|
|
252
|
+
helper_args: tuple[str, ...] = ()
|
|
253
|
+
if helper == "terraform":
|
|
254
|
+
helper_args = _terraform_helper_args(ctx, opts, repo)
|
|
255
|
+
install_kwargs["helper_args"] = helper_args
|
|
256
|
+
|
|
145
257
|
try:
|
|
146
|
-
actions = installer.install(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
credential=opts.credential,
|
|
154
|
-
api_host=opts.api_host,
|
|
155
|
-
)
|
|
258
|
+
actions = installer.install(**install_kwargs)
|
|
259
|
+
except (
|
|
260
|
+
TerraformrcConflictError,
|
|
261
|
+
TerraformHelperExeNotFound,
|
|
262
|
+
TerraformPluginDirError,
|
|
263
|
+
) as exc:
|
|
264
|
+
raise click.ClickException(str(exc))
|
|
156
265
|
except OSError as exc:
|
|
157
266
|
raise click.ClickException(
|
|
158
267
|
f"Failed to install {helper!r} credential helper: {exc}"
|
|
@@ -166,11 +275,17 @@ def install_cmd(
|
|
|
166
275
|
use_stderr = utils.should_use_stderr(opts)
|
|
167
276
|
warnings = [a for a in actions if a.startswith("WARNING")]
|
|
168
277
|
normal = [a for a in actions if not a.startswith("WARNING")]
|
|
278
|
+
|
|
279
|
+
next_steps: list[str] = []
|
|
280
|
+
if helper == "terraform":
|
|
281
|
+
next_steps = _terraform_next_steps(helper_args)
|
|
282
|
+
|
|
169
283
|
data = {
|
|
170
284
|
"helper": helper,
|
|
171
285
|
"dry_run": dry_run,
|
|
172
286
|
"actions": normal,
|
|
173
287
|
"warnings": warnings,
|
|
288
|
+
"next_steps": next_steps,
|
|
174
289
|
}
|
|
175
290
|
if utils.maybe_print_as_json(opts, data):
|
|
176
291
|
sys.exit(ec)
|
|
@@ -181,6 +296,8 @@ def install_cmd(
|
|
|
181
296
|
click.echo(f" {action}" if dry_run else action, err=use_stderr)
|
|
182
297
|
for warning in warnings:
|
|
183
298
|
click.secho(f" {warning}" if dry_run else warning, err=True, fg="yellow")
|
|
299
|
+
for line in next_steps:
|
|
300
|
+
click.secho(line, err=use_stderr, fg="cyan")
|
|
184
301
|
sys.exit(ec)
|
|
185
302
|
|
|
186
303
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright 2026 Cloudsmith Ltd
|
|
2
|
+
"""
|
|
3
|
+
Terraform credentials helper command.
|
|
4
|
+
|
|
5
|
+
Implements the ``get`` verb of Terraform's credentials-helper protocol for
|
|
6
|
+
Cloudsmith registries. The installed ``terraform-credentials-cloudsmith``
|
|
7
|
+
launcher forwards Terraform's invocation to this command.
|
|
8
|
+
|
|
9
|
+
See: https://developer.hashicorp.com/terraform/internals/credentials-helpers
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from ....credential_helpers.terraform import execute
|
|
17
|
+
from ...decorators import (
|
|
18
|
+
common_api_auth_options,
|
|
19
|
+
common_cli_config_options,
|
|
20
|
+
resolve_credentials,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.command(context_settings={"ignore_unknown_options": True})
|
|
25
|
+
@click.option(
|
|
26
|
+
"-r",
|
|
27
|
+
"--repo",
|
|
28
|
+
"--repository",
|
|
29
|
+
"repo",
|
|
30
|
+
required=True,
|
|
31
|
+
envvar="CLOUDSMITH_REPO",
|
|
32
|
+
help="The Cloudsmith repository the registry serves. Terraform does not "
|
|
33
|
+
"pass the repository to a credentials helper, so it must be configured "
|
|
34
|
+
"here (e.g. in the terraformrc args) to build a repository-scoped token.",
|
|
35
|
+
)
|
|
36
|
+
@click.argument("params", nargs=-1, type=click.UNPROCESSED)
|
|
37
|
+
@common_cli_config_options
|
|
38
|
+
@common_api_auth_options
|
|
39
|
+
@resolve_credentials
|
|
40
|
+
def terraform(opts, repo, params):
|
|
41
|
+
"""
|
|
42
|
+
Terraform credentials helper for Cloudsmith registries.
|
|
43
|
+
|
|
44
|
+
Resolves the token for a Cloudsmith Terraform registry and prints it in
|
|
45
|
+
Terraform's expected JSON credentials format: ``{"token": "..."}``.
|
|
46
|
+
|
|
47
|
+
Provides credentials for all Cloudsmith Terraform registries:
|
|
48
|
+
``*.cloudsmith.io``, ``*.cloudsmith.com``, and any custom domains
|
|
49
|
+
configured for the organisation (requires an organisation - ``--org``,
|
|
50
|
+
CLOUDSMITH_ORG or ``org`` in ``config.ini`` - and a valid API key/token).
|
|
51
|
+
|
|
52
|
+
Accepts Terraform's calling convention — an optional verb (``get``,
|
|
53
|
+
``store``, ``forget``) followed by the hostname — so the on-PATH launcher
|
|
54
|
+
can forward Terraform's arguments verbatim. When no verb is given the
|
|
55
|
+
action is ``get``. When no hostname is given it is read from stdin. Only
|
|
56
|
+
``get`` is served; ``store``/``forget`` return an error and a non-zero exit.
|
|
57
|
+
``store`` first drains its stdin payload (the credentials Terraform sends)
|
|
58
|
+
before erroring, as the protocol requires, so Terraform sees the exit status
|
|
59
|
+
rather than a broken pipe.
|
|
60
|
+
|
|
61
|
+
A hostname that is not a Cloudsmith registry yields an empty object
|
|
62
|
+
(``{}``) and exit 0 so Terraform falls back to its own credential sources.
|
|
63
|
+
|
|
64
|
+
\b
|
|
65
|
+
Input (arguments or stdin):
|
|
66
|
+
[VERB] HOSTNAME — e.g. "get terraform.cloudsmith.io" or just
|
|
67
|
+
"terraform.cloudsmith.io"; HOSTNAME alone may also come from stdin.
|
|
68
|
+
|
|
69
|
+
\b
|
|
70
|
+
Output (stdout):
|
|
71
|
+
JSON: {"token": "<cloudsmith-token>"} (Cloudsmith host, token found)
|
|
72
|
+
JSON: {} (not a Cloudsmith host)
|
|
73
|
+
|
|
74
|
+
\b
|
|
75
|
+
Exit codes:
|
|
76
|
+
0: Token returned, or the host is not a Cloudsmith registry
|
|
77
|
+
1: Cloudsmith host with no credentials available, or an error occurred
|
|
78
|
+
|
|
79
|
+
The token is scoped to the repository. On a standard
|
|
80
|
+
``*.cloudsmith.io``/``*.cloudsmith.com`` host it is ``{org}/{repo}/{token}``
|
|
81
|
+
and the organisation is required (``--org``, ``CLOUDSMITH_ORG`` or ``org``
|
|
82
|
+
in ``config.ini``); a standard host requested without an organisation is a
|
|
83
|
+
non-zero exit. On a custom domain — which is already bound to a single
|
|
84
|
+
organisation — the org is omitted and the token is ``{repo}/{token}``. The
|
|
85
|
+
repository is always required (``-r/--repo/--repository`` or
|
|
86
|
+
``CLOUDSMITH_REPO``): Terraform does not tell a credentials helper which
|
|
87
|
+
repository is being requested. A non-Cloudsmith host still returns ``{}``
|
|
88
|
+
and exit 0. The profile can also be supplied with ``-P/--profile``. The
|
|
89
|
+
launcher forwards Terraform's ``args`` verbatim, so a terraformrc block such
|
|
90
|
+
as ``credentials_helper "cloudsmith" { args = ["--org=acme",
|
|
91
|
+
"--repo=my-repo", "-P", "ci"] }`` reaches this command as those options.
|
|
92
|
+
|
|
93
|
+
\b
|
|
94
|
+
Examples:
|
|
95
|
+
# Direct usage
|
|
96
|
+
$ cloudsmith credential-helper terraform --repo my-repo terraform.cloudsmith.io
|
|
97
|
+
{"token": "..."}
|
|
98
|
+
|
|
99
|
+
# Terraform's calling convention (verb + hostname)
|
|
100
|
+
$ cloudsmith credential-helper terraform --repo my-repo get terraform.cloudsmith.io
|
|
101
|
+
|
|
102
|
+
# Select an org and profile explicitly (no env vars needed)
|
|
103
|
+
$ cloudsmith credential-helper terraform --org=acme -P ci get terraform.cloudsmith.io
|
|
104
|
+
|
|
105
|
+
\b
|
|
106
|
+
Environment variables:
|
|
107
|
+
CLOUDSMITH_API_KEY: API key for authentication (optional)
|
|
108
|
+
CLOUDSMITH_ORG: Organisation slug (required to scope the token)
|
|
109
|
+
CLOUDSMITH_REPO: Repository slug the registry serves (required)
|
|
110
|
+
CLOUDSMITH_PROFILE: Configuration profile to load (optional)
|
|
111
|
+
"""
|
|
112
|
+
# Terraform passes "<verb> <hostname>"; direct/manual use may pass just the
|
|
113
|
+
# hostname (verb defaults to "get") or nothing (hostname read from stdin).
|
|
114
|
+
verb = "get"
|
|
115
|
+
hostname: str | None = None
|
|
116
|
+
if len(params) >= 2:
|
|
117
|
+
verb, hostname = params[-2], params[-1]
|
|
118
|
+
elif len(params) == 1:
|
|
119
|
+
hostname = params[0]
|
|
120
|
+
|
|
121
|
+
# The `store` verb sends the new credentials as a JSON object on stdin. The
|
|
122
|
+
# protocol requires an unsupported store implementation to consume the whole
|
|
123
|
+
# payload before returning an error, otherwise Terraform can see a broken
|
|
124
|
+
# pipe instead of the helper's exit status. Drain stdin here; the payload is
|
|
125
|
+
# discarded because this helper never stores credentials.
|
|
126
|
+
if verb == "store":
|
|
127
|
+
try:
|
|
128
|
+
sys.stdin.read()
|
|
129
|
+
except (OSError, ValueError):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
if not hostname:
|
|
133
|
+
try:
|
|
134
|
+
hostname = sys.stdin.read().strip()
|
|
135
|
+
except (OSError, ValueError):
|
|
136
|
+
hostname = ""
|
|
137
|
+
|
|
138
|
+
exit_code, stdout, stderr = execute(
|
|
139
|
+
verb,
|
|
140
|
+
hostname,
|
|
141
|
+
credential=opts.credential,
|
|
142
|
+
api_host=opts.api_host,
|
|
143
|
+
org=opts.org,
|
|
144
|
+
repo=repo,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if stdout is not None:
|
|
148
|
+
click.echo(stdout)
|
|
149
|
+
if stderr is not None:
|
|
150
|
+
click.echo(stderr, err=True)
|
|
151
|
+
sys.exit(exit_code)
|