Skip to content

feat: add Docker credential helper for Cloudsmith registries#277

Merged
BartoszBlizniak merged 23 commits into
masterfrom
iduffy/credential-helper-base
Jun 10, 2026
Merged

feat: add Docker credential helper for Cloudsmith registries#277
BartoszBlizniak merged 23 commits into
masterfrom
iduffy/credential-helper-base

Conversation

@cloudsmith-iduffy

@cloudsmith-iduffy cloudsmith-iduffy commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Description

Implement the Docker credential helper protocol so Docker can automatically authenticate with Cloudsmith registries (including custom domains) without manual docker login.

Key changes:

  • Add cloudsmith credential-helper docker CLI command
  • Add docker-credential-cloudsmith wrapper binary (entry point)
  • Add custom domain discovery via Cloudsmith API with filesystem caching

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Other (please describe)

Additional Notes

Manually tested with:

  • Cloudsmith cli configured with CLOUDSMITH_API_KEY
  • Cloudsmith cli configured with CLOUDSMITH_API_KEY and CLOUDSMITH_ORG and a custom domain

@cloudsmith-iduffy cloudsmith-iduffy requested a review from a team as a code owner March 14, 2026 00:37
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch from 31f616c to 58327a3 Compare March 14, 2026 13:42
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-provider-chain branch 2 times, most recently from 8b57884 to 0b0445c Compare March 14, 2026 14:06
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch from 58327a3 to 910a2cd Compare March 14, 2026 14:09
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-provider-chain branch from 0b0445c to 987c32f Compare March 14, 2026 14:10
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch from 910a2cd to 23ab3ad Compare March 14, 2026 14:12
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-provider-chain branch 2 times, most recently from 65d8c53 to 646c50a Compare March 14, 2026 14:23
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch from 23ab3ad to 0e03731 Compare March 14, 2026 14:24
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-provider-chain branch from 646c50a to 5c2b23d Compare March 14, 2026 14:26
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch 2 times, most recently from 641bec5 to 5540a76 Compare March 14, 2026 14:39
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-provider-chain branch from 6e1792c to 8862812 Compare March 14, 2026 14:43
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch from 5540a76 to 2aad86a Compare March 14, 2026 14:45
@cloudsmith-iduffy cloudsmith-iduffy marked this pull request as draft March 14, 2026 14:49
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-provider-chain branch 2 times, most recently from 368db92 to a60887d Compare March 15, 2026 10:34
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch 2 times, most recently from 4e72204 to 21fe5cf Compare March 15, 2026 21:43
@cloudsmith-iduffy cloudsmith-iduffy marked this pull request as ready for review March 15, 2026 22:24
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-provider-chain branch from 1cef871 to b4b2583 Compare March 25, 2026 12:13
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch from 21fe5cf to d2cd0d1 Compare March 25, 2026 12:20
Comment thread cloudsmith_cli/credential_helpers/custom_domains.py Outdated
Comment thread setup.py
Comment on lines +68 to 72
"console_scripts": [
"cloudsmith=cloudsmith_cli.cli.commands.main:main",
"docker-credential-cloudsmith=cloudsmith_cli.credential_helpers.docker.wrapper:main",
]
},

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.

question: Have you tested this with the pyz ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It won't work with pyz, pyz just gives a single binary. It is good you call this out though, we will have a documentation task to have the user create their own wrapper to cloudsmith credential-helper docker

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.

Should revisit this comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bart looks to have workaround this by introducing an install command.

Comment thread cloudsmith_cli/credential_helpers/docker/__init__.py Outdated
Comment thread cloudsmith_cli/cli/commands/credential_helper/docker.py Outdated
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-provider-chain branch from 74f874c to f30e428 Compare March 31, 2026 16:18
@cloudsmith-iduffy cloudsmith-iduffy force-pushed the iduffy/credential-helper-base branch from d2cd0d1 to 05fd479 Compare March 31, 2026 21:35
Base automatically changed from iduffy/credential-provider-chain to master May 20, 2026 09:49
Comment thread cloudsmith_cli/core/credentials/oidc/cache.py Outdated
Comment thread cloudsmith_cli/credential_helpers/common.py Outdated
Comment thread cloudsmith_cli/credential_helpers/common.py Outdated
Comment thread cloudsmith_cli/credential_helpers/custom_domains.py Outdated
Comment thread cloudsmith_cli/credential_helpers/custom_domains.py Outdated
Comment thread setup.py
Comment on lines +68 to 72
"console_scripts": [
"cloudsmith=cloudsmith_cli.cli.commands.main:main",
"docker-credential-cloudsmith=cloudsmith_cli.credential_helpers.docker.wrapper:main",
]
},

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.

Should revisit this comment

Comment thread cloudsmith_cli/credential_helpers/custom_domains.py Outdated
import sys


def main():

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.

question: any reason why this can't be a CLI command instead of a separate script?

BartoszBlizniak and others added 11 commits June 3, 2026 15:30
Extends cache_utils.py with merge_json_file, a safe atomic JSON-merge
writer for editing user-owned config files (e.g. ~/.docker/config.json).
Supports read-or-empty fallback, in-place mutation, stable indent=2
serialisation, idempotent change detection, optional .bak backup,
dry_run mode, parent-dir creation, and atomic temp+replace writes.
Extracts _atomic_write_text as a shared private helper so atomic_write_json
retains its public signature and behaviour.

Adds 27 tests covering all nine specified behaviours (foreign-key
preservation, missing file/parent-dir creation, backup semantics,
idempotency, dry_run, malformed input, serialisation format, return value,
and atomic_write_json round-trip + permissions).

Co-Authored-By: Claude <noreply@anthropic.com>
…filter

Reshape the custom-domain layer to return typed CustomDomain dataclass
records (host, backend_kind, enabled, validated) instead of bare host
strings. Add BackendKind IntEnum mirroring the server enum. Expose
get_custom_domains() and get_format_domains() so callers can filter by
format. Switch list_custom_domains() in core/api/orgs.py to return
to_dict() records following the repo pattern. Tighten exception handling
to ApiException-only (no bare except). Rewire is_cloudsmith_domain to
use enabled+validated check. Update tests for the new structured API.

Co-Authored-By: Claude <noreply@anthropic.com>
…handling

Move Docker credential-helper protocol logic from the click command into a
transport-light `credential_helpers/docker/runtime.py`. The command is now a
thin shim that calls `execute()` and passes stdout/stderr/exit-code back to the
caller. The D17 protocol-boundary broad-except in `_execute_get` ensures that
network/SDK errors degrade to a clean exit-1 refusal rather than crashing
`docker pull`/`push`. `credentials.py` is removed; its logic lives in `runtime.py`.

Co-Authored-By: Claude <noreply@anthropic.com>
Replaces the old Python wrapper entry-point with an on-PATH shell launcher
written by `cloudsmith credential-helper install docker`. The installer
patches ~/.docker/config.json via merge_json_file (preserving foreign keys,
atomic write, .bak backup) and supports --domain, --bin-dir, and --dry-run.
Adds launchers.py (write/remove/resolve_bin_dir/is_on_path), docker/installer.py
(DockerInstaller), and cli/commands/credential_helper/manage.py
(install/uninstall/list commands). Removes credential_helpers/docker/wrapper.py
and its setup.py console_scripts entry point.

Co-Authored-By: Claude <noreply@anthropic.com>
Wires custom-domain autodiscovery into `credential-helper install docker`.
Discovery is best-effort: if org/credentials are absent, or if the API call
fails, the default host is still registered and a clear info/warning action
is returned.

- custom_domains.py: add `refresh: bool = False` to `get_custom_domains`
  and `get_format_domains`; when True, skips the cache read and always
  fetches from the API (writing the fresh result back to cache).
- installer.py: extend `DockerInstaller.install` with `discover`, `refresh`,
  `org`, `api_key`, `auth_type`, `api_host` parameters; adds a single
  deliberate broad-except discovery boundary with `# pylint: disable=broad-except`.
- manage.py: add `--no-discover`, `--refresh`, `--org` (envvar CLOUDSMITH_ORG)
  options; apply `@common_api_auth_options` + `@resolve_credentials`; thread
  all new params into `installer.install`.
- test_credential_helper_install.py: new test classes covering discovery on/off,
  missing org/creds skip, failure graceful degradation, dedup, --refresh
  cache-bypass, and CliRunner smoke tests for the new CLI flags.

Co-Authored-By: Claude <noreply@anthropic.com>
Add a `bin_dir` keyword param to `DockerInstaller.uninstall` and wire it
through to `resolve_bin_dir`, so that `uninstall docker --bin-dir /custom`
locates and removes the launcher written by `install --bin-dir /custom`
instead of silently looking in the auto-resolved directory. Thread the new
`--bin-dir` Click option into `uninstall_cmd` in manage.py (matching the
help-text style of the install counterpart). Two on-disk tests verify the
fix: one confirms the launcher is gone after a matching uninstall, the other
confirms a mismatched dir leaves the launcher intact.

Co-Authored-By: Claude <noreply@anthropic.com>
…uninstall/list

Add -F/--output-format {pretty,json,pretty_json} to the three
credential-helper management commands (install, uninstall, list) following
the repo convention. JSON mode emits {"data": ...} on stdout with no
human text; pretty mode is unchanged. The runtime docker command is
untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
… domains

Pass BackendKind.DOCKER to is_cloudsmith_domain so the Docker credential
helper only vouches for Docker-format custom domains. Adds backend_kind
optional param to is_cloudsmith_domain (None default preserves existing
generic behavior); updates docker/runtime.py to pass BackendKind.DOCKER;
extends TestIsCloudsmithDomain and adds TestDockerRuntimeBackendKindFiltering
to cover filtering correctness.

Co-Authored-By: Claude <noreply@anthropic.com>
Collapse 108 tests (45 + 63) down to 61 (34 + 27) by merging near-identical
cases into parametrized tests and removing layer-duplicates. All five retained
guards survive: broken-pipe boundary, legacy-cache format miss, uppercase-host
casing, status str-not-Path serialisation, and graceful discovery failure.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread cloudsmith_cli/cli/tests/commands/test_credential_helper_install.py Dismissed
Comment thread cloudsmith_cli/cli/tests/commands/test_credential_helper_install.py Dismissed
The launcher/resolve tests monkeypatched os.name="nt" on a posix host to
exercise the Windows code paths. On Python < 3.12 that makes pathlib.Path
build a WindowsPath, which raises NotImplementedError — crashing the whole
pytest session in CI (3.11) during failure-report/cache-write. It passed
locally only because 3.12+ rewrote pathlib to tolerate it.

Extract the platform-specific logic (launcher filename, script body, user bin
dir) into pure helpers parameterised on `windows: bool`, and test those
directly instead of faking os.name. Public API and return types unchanged;
behaviour on real Windows is identical.

Co-Authored-By: Claude <noreply@anthropic.com>

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

Copilot reviewed 19 out of 20 changed files in this pull request and generated 10 comments.

Comment thread cloudsmith_cli/credential_helpers/docker/installer.py
Comment thread cloudsmith_cli/credential_helpers/docker/installer.py
Comment thread setup.py
Comment thread cloudsmith_cli/credential_helpers/common.py
Comment thread cloudsmith_cli/credential_helpers/docker/__init__.py
Comment thread cloudsmith_cli/credential_helpers/__init__.py
Comment thread cloudsmith_cli/cli/commands/credential_helper/__init__.py
Comment thread cloudsmith_cli/cli/commands/credential_helper/docker.py
Comment thread cloudsmith_cli/credential_helpers/launchers.py Outdated
Comment thread cloudsmith_cli/credential_helpers/launchers.py Outdated
BartoszBlizniak and others added 2 commits June 8, 2026 17:21
…opilot review)

Coerce non-dict credHelpers to {} on install (Fix 1), guard non-dict
credHelpers as a no-op on uninstall (Fix 2), pass newline="" to
write_text to prevent \r\r\n on Windows (Fix 3), and require W_OK|X_OK
when selecting candidate bin dir (Fix 4). Add tests 17 and 18 covering
the two malformed-credHelpers edge cases.

Co-Authored-By: Claude <noreply@anthropic.com>
…ot review)

Add missing '# Copyright 2026 Cloudsmith Ltd' header to five new source
files flagged by Copilot for inconsistent licensing headers. Matches the
convention established in cache_utils.py and backends.py exactly.

Co-Authored-By: Claude <noreply@anthropic.com>
@BartoszBlizniak

Copy link
Copy Markdown
Member
  • Dropped the standalone wrapper + the docker-credential-cloudsmith entry point. It's now just cloudsmith credential-helper docker <get|store|erase|list>
  • New install/uninstall/list commands. install docker writes the on-PATH launcher (a tiny shim that execs back into cloudsmith) and merges an entry into ~/.docker/config.json . --bin-dir/--domain/--dry-run to control it.
  • Custom-domain auto-discovery now goes through the SDK. Scoped per-format (backend_kind==DOCKER), so only Docker custom domains get registered.
  • Split Docker into credential_helpers/docker/ (runtime + installer) so format-specific logic is isolated; the discovery/launcher/writer bits are shared for a future npm/etc. helper.

@alancarson alancarson dismissed estebangarcia’s stale review June 10, 2026 08:38

Ian asked me to!

@BartoszBlizniak BartoszBlizniak merged commit fc7dacc into master Jun 10, 2026
40 checks passed
@BartoszBlizniak BartoszBlizniak deleted the iduffy/credential-helper-base branch June 10, 2026 08:56
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.

7 participants