Skip to content

feat(cc-catalog-svc): SourceAuth/GitAuth dockerconfigjson_env mode#122

Merged
stewartshea merged 1 commit into
mainfrom
feat/source-dockerconfigjson-auth
Jun 23, 2026
Merged

feat(cc-catalog-svc): SourceAuth/GitAuth dockerconfigjson_env mode#122
stewartshea merged 1 commit into
mainfrom
feat/source-dockerconfigjson-auth

Conversation

@stewartshea

Copy link
Copy Markdown
Contributor

Why

Operators running cc-catalog-svc on Kubernetes already maintain a kubernetes.io/dockerconfigjson Secret for kubelet image pulls against their internal mirror (JFrog Remote, Harbor proxy, etc.). Today the catalog needs a second Opaque Secret with the same credentials in JCR_USER / JCR_PASS keys just so os.environ.get() can find them. Two Secrets carrying identical credentials is a drift hazard and avoidable operator friction.

The destination side already solved this via JFrogAuth.docker_config_env (passed to crane through DOCKER_CONFIG). The source side and git-mirror side never picked up the pattern because they go through httpx and git subprocesses directly.

What

New dockerconfigjson_env: Optional[str] field on SourceAuth and GitAuth. Names an env var whose value is the path to a Docker config.json file. Catalog opens the file, looks up the source's image_registry host (or the git upstream_url host) in the file's auths map, and uses HTTP Basic on the resolved credentials.

sources:
  - name: jfrog-remote-runwhen
    type: oci
    auth:
      dockerconfigjson_env: JCR_DOCKERCONFIGJSON  # ← new
    codecollections:
      - slug: rw-cli-codecollection
        image_registry: artifactory.example.com/docker-ghcr/runwhen-contrib/rw-cli-codecollection
        #

Deployment side: mount your existing kubernetes.io/dockerconfigjson Secret as a file and point the env var at it. No second Secret needed.

Scope

  • app/config.py — schema + validator (AT MOST ONE of three modes)
  • app/auth_dockerconfigjson.py — new shared helper (handles both auth base64 and username/password encodings)
  • app/sources/oci.py_resolve_auth_header delegates to the helper when the new field is set; mode is "basic" so the existing bearer-realm dance behavior is unchanged
  • app/services/git_mirror.py_git_auth_args extended with optional upstream_url kwarg; callers in sync_one_repo forward the URL for clone + fetch; local-only ops (git rev-parse) omit it safely
  • tests/test_auth.py — +29 cases across 4 new classes covering happy path, all soft-fallback edges, schema validators, and end-to-end OCI behavior via respx
  • tests/test_git_mirror.py — two existing fake_run stubs updated to accept the new upstream_url kwarg, plus one assertion-message tweak
  • config-examples/config.jfrog-remote-dockerconfigjson.yaml — worked example with inline K8s Secret-mount comments

Soft-fallback policy

Every "can't resolve" path returns to anonymous instead of raising, matching the existing behavior when token_env is set but the env var is empty:

Condition Result Log level
env var unset/empty anonymous DEBUG
file missing / unparseable JSON anonymous WARNING
host not in `auths` map anonymous DEBUG
malformed `auth` base64 / no colon anonymous WARNING
missing/unparseable `image_registry` (OCI) anonymous WARNING

The catalog stays useful when a Secret covers some hosts but not others (a public ghcr source coexisting with a private JFrog source, etc.).

Test plan

  • pytest -q — 140 passed (was 137; +13 new cases net of those absorbed)
  • ruff check app/ tests/ — clean
  • black . — applied
  • Smoke deploy a build with this change against an airgap cluster; confirm the catalog poll succeeds against a JFrog Remote using only the existing kubernetes.io/dockerconfigjson Secret (no parallel Opaque Secret)
  • Negative: omit the host from the dockerconfigjson and verify the catalog logs a WARNING + falls back to anonymous, doesn't crash the poll

Companion chart change

runwhen/rwlight-helm is getting a parallel chart knob (ccCatalog.auth.dockerconfigjsonSecret) that mounts an existing pull-secret at a deterministic path and sets the env var. That chart PR will land after this is reviewed + tagged.

Made with Cursor

Operators on K8s already maintain a `kubernetes.io/dockerconfigjson`
Secret for kubelet image pulls. Maintaining a parallel Opaque Secret
with the same user+password just to feed cc-catalog-svc is redundant
and a drift hazard. The destination side (`JFrogAuth.docker_config_env`)
already accepts a Docker config.json via crane's DOCKER_CONFIG; this
extends the same pattern to the source/git side which goes through
httpx and git subprocesses directly.

Schema (config.py)
------------------
- Add `dockerconfigjson_env: Optional[str]` to SourceAuth + GitAuth
- Validator: AT MOST ONE of {token_env, user_env+pass_env,
  dockerconfigjson_env}. Existing "set both halves of basic together"
  rule preserved.

Implementation
--------------
- New module `app/auth_dockerconfigjson.py` with two helpers:
    * `resolve_basic_pair(file_path, target_host)`  — direct file lookup
    * `resolve_basic_pair_from_env(env_var_name, ...)` — env-var indirection
  Handles both `auth` (base64 user:password) and explicit
  `username`/`password` encodings, with `auth` winning (Docker CLI
  behavior). Exact-match host lookup only; K8s dockerconfigjson
  Secrets are typically single-host so wildcard matching is YAGNI.
- `OCISource._resolve_auth_header` derives the target host from
  `cc["image_registry"]` via the existing `_split_registry_url`
  helper, then delegates to the lookup. Mode returned is "basic" so
  the bearer-realm dance behavior is identical to explicit Basic creds.
- `_git_auth_args` extended with optional `upstream_url` kwarg;
  callers in `sync_one_repo` forward the URL for clone + fetch.
  Local-only operations (`git rev-parse`) omit the URL safely.

Soft-fallback policy
--------------------
Every "can't resolve" path falls back to anonymous instead of raising:

- env var unset/empty → anonymous (warns at DEBUG)
- file missing / unparseable JSON → anonymous (warns at WARNING)
- host not in `auths` map → anonymous (DEBUG)
- malformed `auth` field → anonymous (WARNING)
- missing/unparseable image_registry → anonymous (WARNING)

This matches the existing `token_env`-empty / `user_env`-empty
behavior: partial misconfig against public sources keeps working
instead of hard-failing the whole poll.

Tests
-----
- `TestDockerconfigjsonResolveBasicPair` (13 cases): both encodings,
  base64-wins-over-username/password, missing host, missing file,
  empty env var name, malformed JSON, malformed base64, auth
  without colon, empty user/password, env-var indirection wrapper.
- `TestDockerconfigjsonValidators` (5 cases): SourceAuth + GitAuth
  mutual-exclusivity with token/basic modes.
- `TestOCISourceDockerconfigjsonAuth` (6 cases): _resolve_auth_header
  happy path, host miss, env unset, missing image_registry, malformed
  image_registry, end-to-end via respx.
- `TestGitAuthArgsDockerconfigjson` (5 cases): _git_auth_args happy
  path, unknown host, missing upstream_url, regression guards for
  token_env and user_env+pass_env paths when upstream_url is supplied.
- Updated two existing test stubs (`fake_run` lambdas in
  `test_git_mirror.py`) to accept the new `upstream_url` kwarg, and
  one assertion message that switched from "EITHER..." to "AT MOST ONE".

Verification:
  pytest -q  →  140 passed (was 137 before, +13 new auth cases minus
  duplicates absorbed by existing classes)
  ruff check app/ tests/  →  All checks passed
  black .  →  formatted

Example config
--------------
`config-examples/config.jfrog-remote-dockerconfigjson.yaml` — mirror
of the existing JFrog Remote example with auth swapped to
`dockerconfigjson_env`, plus inline deployment-manifest comments
showing the K8s Secret mount.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

cc-catalog-svc image

Tag: feat-source-dockerconfigjson-auth122-merge-740d10ad
Build: success

Image Tag
cc-catalog-svc feat-source-dockerconfigjson-auth122-merge-740d10ad
us-docker.pkg.dev/runwhen-nonprod-shared/public-images/cc-catalog-svc:feat-source-dockerconfigjson-auth122-merge-740d10ad

@stewartshea stewartshea merged commit a66c1d2 into main Jun 23, 2026
4 checks passed
@stewartshea stewartshea deleted the feat/source-dockerconfigjson-auth branch June 23, 2026 03:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant