Skip to content

Add private registry login + registry-aware pull - #10

Merged
kylemclaren merged 2 commits into
mainfrom
registry-login
Jul 5, 2026
Merged

kylemclaren merged 2 commits into
mainfrom
registry-login

Conversation

@kylemclaren

Copy link
Copy Markdown
Owner

What

Ship the ability to authenticate to private registries from ContainerUI, and make the pull sheet registry-aware.

Wraps Apple's container registry login|logout|list. Pull from a private registry already worked once you were logged in — this adds the login UI that unlocks it, plus a hint so a private pull doesn't fail cryptically when you're not signed in.

(Registry browse is deliberately out of scope — private registries speak the OCI Distribution API, not Docker Hub's search, so Explore stays Docker-Hub-only.)

Changes

Core

  • RegistryService — login (password piped via --password-stdin, never in argv), logout, list (rich JSON), loggedInHosts (from --quiet).
  • RegistryLogin — tolerant Decodable (hostname/host/server, username/user), since registry list --format json returns [] today and its keys are unverifiable.
  • RegistryHost — normalizes hosts and folds Docker Hub aliases (docker.io, index.docker.io, …) to one key.

App / UI

  • System screen: a Registries card — lists logins, log out per row, "Log in" sheet.
  • RegistryLoginView — server / username / password (SecureField) / scheme sheet, mirroring CreateVolumeView.
  • Pull sheet: a non-blocking InlineBanner — "Not signed in to ghcr.io" with a prefilled Log in action. Docker Hub / public pulls never nag; the hint never gates a pull.

Design notes

  • The pull hint's source of truth is --quiet (hostname-only, format-stable), not JSON — so a JSON key rename can't break the safety-critical path. JSON only enriches the management list, and falls back to --quiet rows if it fails to decode.
  • loggedInRegistries is Set<String>?: nil = unknown ⇒ hint stays silent (fail safe, never a false nag).
  • Password sent as stdin Data with no trailing newline; not a parameter of any argument builder.

Testing

  • 115 tests pass (adds registry arg-builder — asserting the password is absent from argv — service, tolerant-decoding, and RegistryHost normalization suites).
  • xcodebuild build clean (per repo convention, the test action skips the SwiftUI app target, so the app target was built separately to catch view compile errors).

Runtime QA against a live private registry (actual login/pull) is worth a manual pass before merge.

🤖 Generated with Claude Code

kylemclaren and others added 2 commits July 5, 2026 20:00
Wraps `container registry login|logout|list` so you can authenticate to
private registries from the app, and teaches the pull sheet to notice when
the target registry isn't signed in.

- RegistryService: login (password piped via --password-stdin, never argv),
  logout, list. RegistryLogin decodes `registry list --format json` with a
  tolerant decoder; the pull hint's source of truth is the format-stable
  `--quiet` host list, so a JSON key rename can't break it.
- RegistryHost normalizes hosts and folds Docker Hub aliases to docker.io.
- System screen gains a Registries card (list + log in/out); the Pull sheet
  shows a non-blocking "not signed in to X" hint with a prefilled login sheet.
  Public/Docker Hub pulls never nag, and the hint never gates a pull.
- Tests: registry arg builders (asserts password absent from argv), service
  (stdin password, failure, quiet-list normalization, JSON decode), tolerant
  decoding, and RegistryHost normalization. 115 tests pass; app builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- RegistryLogin: decode the REAL `container registry list --format json` keys
  (`name`/`id` for host, `creationDate`/`modificationDate` for dates). The
  guessed `hostname`/`host`/`server`/`created`/`modified` never matched, so
  list() always threw and the card silently degraded to hostname-only with a
  blank username. Fixtures/tests now use real CLI output instead of false-green
  invented keys. (Verified against the live CLI.)
- ExploreScreen: inject `.environment(app)` on its PullImageView sheet — the
  second call site was missed when PullImageView gained an AppModel dependency,
  risking a crash on Pull-from-Explore. (ImagesScreen already had it.)
- RegistriesSection: clear `errorMessage` at the top of reload() so a stale
  error no longer sticks under a healthy list; suppress the "signed in to
  nothing" line while an error is showing.
- Logout correctness: the decode-failure fallback now uses verbatim `--quiet`
  hostnames (new loggedInHostsRaw), not normalized ones — logout(server:) must
  send the exact stored name or it won't match the credential.

116 tests pass; app builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kylemclaren

Copy link
Copy Markdown
Owner Author

Adversarial review — done, findings fixed

Ran a multi-lens adversarial review (7 finder lenses → each finding refuted by 3 diverse skeptics, majority-confirm to survive → completeness critic). 16 findings raised, 10 survived verification. The material ones are now fixed in cb80252:

Fixed

  • JSON keys were wrong (HIGH). The real container registry list --format json emits the host under name/id and dates under creationDate/modificationDate — the guessed hostname/host/server never matched, so list() always threw and the card silently degraded to hostname-only with a blank username. The tests were false-green (invented-key fixtures). Now decodes the real shape (verified against the live CLI) with the old spellings kept as tolerant fallbacks; fixtures/tests use real output.
  • Explore's Pull sheet missed .environment(app) (crash risk). PullImageView gained an AppModel dependency and ImagesScreen's sheet was updated, but ExploreScreen's second call site wasn't — Pull-from-Explore could trap. Injected.
  • Stale errorMessage in RegistriesSection.reload(). A transient error stuck under a later-healthy list. Cleared at the top of reload; empty-state text suppressed while an error shows.
  • Logout used a normalized host. The decode-failure fallback now uses verbatim --quiet hostnames (new loggedInHostsRaw), so logout(server:) sends the exact stored name.

Deferred (by-design or low, with rationale)

  • HTTP scheme has no cleartext warning — intentional for localhost/plain registries.
  • Docker Hub private repos get no hint — intentional (avoids nagging on every public pull); reviewers largely refuted it.
  • reload() issues two registry list calls — user-initiated, not on any polling path.

116 tests pass; app builds clean.

@kylemclaren
kylemclaren merged commit 5467dc7 into main Jul 5, 2026
1 check passed
@kylemclaren
kylemclaren deleted the registry-login branch July 5, 2026 18:38
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