Commit fc7dacc
feat: add Docker credential helper for Cloudsmith registries (#277)
* feat: add credential provider chain concept
* fix: review feedback
* feat: add Docker credential helper for Cloudsmith registries
* Potential fix for pull request finding 'CodeQL / Incomplete URL substring sanitization'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* fix: copilot feedback
* fix: cache file handling
* fix: move to top level import
* fix: import level and add .com to CS domains
* refactor: change custom domain calls to use sdk
* feat(credential-helper): add merge_json_file safe JSON writer
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>
* feat(credential-helper): structured custom-domain discovery + format 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>
* refactor(credential-helper): extract docker runtime + boundary error 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>
* feat(credential-helper): add install/uninstall/list command + launchers
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>
* feat(credential-helper): auto-discover Docker custom domains on install
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>
* fix(credential-helper): honor --bin-dir on uninstall
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>
* feat(credential-helper): support -F/--output-format json for install/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>
* feat(credential-helper): scope docker runtime to docker-format custom 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>
* test(credential-helper): consolidate tests to one-per-behavior
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>
* fix(credential-helper): make launcher tests Python 3.11-safe
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>
* fix(credential-helper): harden config merge + exact launcher bytes (copilot 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>
* chore(credential-helper): add copyright headers to new modules (copilot 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>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: BB <55028730+BartoszBlizniak@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Bartosz Blizniak <bblizniak@cloudsmith.io>
Co-authored-by: Claude <noreply@anthropic.com>1 parent d7ae816 commit fc7dacc
20 files changed
Lines changed: 3313 additions & 5 deletions
File tree
- cloudsmith_cli
- cli
- commands
- credential_helper
- tests/commands
- core
- api
- credentials/oidc
- tests
- credential_helpers
- docker
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
Lines changed: 39 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
0 commit comments