Skip to content

feat(acrauth): add ACR login helper that writes container auth files - #321

Merged
openshift-merge-bot[bot] merged 7 commits into
Azure:mainfrom
weherdh:acr-auth-for-oc-mirror
Sep 3, 2026
Merged

feat(acrauth): add ACR login helper that writes container auth files#321
openshift-merge-bot[bot] merged 7 commits into
Azure:mainfrom
weherdh:acr-auth-for-oc-mirror

Conversation

@weherdh

@weherdh Wenqi He (weherdh) commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Why

The oc-mirror image authenticates to ACR by running az acr login with DOCKER_COMMAND
pointed at a shim script, so that az's internal docker login call is intercepted and the
credential scraped out of argv. Two problems:

  1. It broke silently when azure-cli 2.88.0 switched to --password-stdin — the shim's
    positional args shifted and it wrote a garbage credential. Mirroring reported success
    while syncing nothing.
  2. The shim's jq rewrite overwrote the auth file, clobbering the registry.redhat.io
    credentials an init container had already written there.

Coupling our auth to the internals of a CLI we don't pin is not a stable contract.

What

This is to resolve https://redhat.atlassian.net/browse/AROSLSRE-1819

A small acrauth login command that does the exchange directly:

  • acr.go — trades the managed identity's Entra token for an ACR refresh token, with
    bounded retry for role-assignment propagation.
  • authfile.goupserts a single registry key into the container auth file.
    Round-trips through map[string]any so other registries and fields we don't model
    (email, identitytoken) survive. Writes via temp file + atomic rename.

Tests cover the merge-preserves-other-registries case, credential replacement, cold start
with file-mode assertion, and empty input.

Note on #257

This overlaps with the ACR token exchange in #257, where the equivalent helpers are
unexported. Happy to rebase onto that and drop acr.go if it lands first — or the reverse.
The rest is disjoint: #257 replaces on-demand.sh and copies via ORAS in-process, so it
never writes an auth file; oc-mirror is an external binary that must read one.

Follow-up

The ARO-HCP change (thin main, Dockerfile, mirror.sh, deleting the shim) lands
separately once this is available as a pseudo-version.

Copilot AI lite review requested due to automatic review settings August 21, 2026 04:54
@openshift-ci

openshift-ci Bot commented Aug 21, 2026

Copy link
Copy Markdown

Hi Wenqi He (@weherdh). Thanks for your PR.

I'm waiting for a Azure member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Tip

We noticed you've done this a few times! Consider joining the org to skip this step and gain /lgtm and other bot rights. We recommend asking approvers on your previous PRs to sponsor you.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

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

Adds a new tools/acrauth Go module intended to provide a stable, CLI-internal ACR authentication flow (managed identity → ACR refresh token) and safely upsert the resulting credential into a container auth file without clobbering other registry entries.

Changes:

  • Introduces acrauth login Cobra command with option validation/completion and Azure credential selection (default vs user-assigned MI).
  • Implements ACR refresh-token exchange (with bounded retry) and auth-file upsert logic that preserves unrelated registries/fields and writes via temp file + rename.
  • Wires the new module into the repo workspace (go.work) and adds module/workspace dependency sums.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tools/acrauth/options.go CLI flag binding and options lifecycle; constructs Azure credential and performs login flow.
tools/acrauth/command.go Defines the login Cobra command and runs validation/completion/login with interrupt-aware context.
tools/acrauth/acr.go Implements Entra/ARM token → ACR refresh token exchange and retry loop for role propagation delays.
tools/acrauth/authfile.go Upserts a single registry credential into a container auth JSON file via read/merge + atomic replace.
tools/acrauth/authfile_test.go Unit tests covering merge-preservation, replacement, file creation/permissions, and empty-registry rejection.
tools/acrauth/go.mod New module definition and direct/indirect dependency list for the acrauth tool.
tools/acrauth/go.sum Checksums for the new module’s dependencies.
go.work Adds ./tools/acrauth to the workspace modules list.
go.work.sum Updates workspace sums to account for newly introduced dependencies.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/acrauth/options.go Outdated
Comment thread tools/acrauth/options.go Outdated
Copilot AI review requested due to automatic review settings August 21, 2026 07:10

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 8 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tools/acrauth/acr.go:40

  • acrFQDN is treated as a bare registry host, but if a caller passes a URL (e.g. https://myregistry.azurecr.io) or includes a path/trailing slash, fmt.Sprintf("https://%s", acrFQDN) will produce an invalid endpoint and a misleading hostname (e.g. host becomes https). Consider validating the input and failing fast with a clear error.
// ExchangeForRefreshToken trades an Entra token for an ACR refresh token, which is what
// container tooling stores as the registry password.
func ExchangeForRefreshToken(ctx context.Context, cred azcore.TokenCredential, acrFQDN string) (string, error) {
	endpoint, err := url.Parse(fmt.Sprintf("https://%s", acrFQDN))
	if err != nil {
		return "", fmt.Errorf("failed to parse ACR endpoint: %w", err)
	}

tools/acrauth/acr.go:82

  • This loop won't compile: range cannot be used over an int constant. Use a conventional counter loop so the retry behavior actually builds and runs.
	for attempt := range retryAttempts {

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 8 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

tools/acrauth/command.go:31

  • The command ignores positional args; without an Args validator, extra arguments are accepted silently even though Use: "login" implies none. Add Args: cobra.NoArgs so user mistakes fail fast.
	cmd := &cobra.Command{
		Use:           "login",
		Short:         "Authenticate to an Azure Container Registry and record the credential in a container auth file",
		SilenceUsage:  true,
		SilenceErrors: true,

Comment thread tools/acrauth/authfile.go Outdated
Copilot AI review requested due to automatic review settings August 25, 2026 04:04

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 8 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

tools/acrauth/authfile.go:110

  • tmp.Write(raw) can (rarely) return a short write with err == nil, which would produce a truncated auth file and break subsequent reads. It’s safer to assert the full buffer was written before proceeding.
	if _, err := tmp.Write(raw); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("failed to write temporary auth file: %w", err)
	}
	if err := tmp.Close(); err != nil {

Comment thread tools/acrauth/acr.go Outdated
@weherdh

Copy link
Copy Markdown
Contributor Author

Validated end-to-end against a live ACR, driven from the consumer PR (Azure/ARO-HCP#6668) with a local replace pointing at this branch.

Against arohcpocpdev (dev):

Check Result
acrauth login token exchange exit 0
Auth file accepted by an independent registry client (oras repo ls --registry-config) authenticated, listed repos
Pre-existing registry.redhat.io entry, incl. its email field preserved
File mode 0600

Full container run — oc-mirror image built with this binary, real mirror, no --dry-run:

Success copying registry.access.redhat.com/ubi9/ubi-micro:latest ➡️ arohcpocpdev.azurecr.io/ubi9/
 ✓  1 / 1 additional images mirrored successfully

So oc-mirror authenticated to the registry and pushed using only the credential this package wrote.

Two bugs surfaced during that testing and are already fixed in this branch:

  • rename(2)EBUSY. The oc-mirror dry-run targets bind-mount auth.json itself, and you cannot rename over a mount point. The atomic write failed and the login aborted. Now falls back to an in-place write (9dda67a), with an explicit Chmod because os.WriteFile only applies its mode on create — observed as 0644 in a container run (e6d42e1).
  • Empty refresh token. A non-nil pointer to "" would have been written out as a credential and reported as success — the same silent-success failure this tool exists to remove (61471d2).

Only the managed-identity branch of newCredential is untested locally, since a UAMI only exists in the deployed container; the CLI branch exercises the same azcore.TokenCredential path.

@raelga

Copy link
Copy Markdown
Collaborator

/ok-to-test

@raelga Rael Garcia (raelga) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR-standard note: please link a Jira or GitHub tracking issue as required by CONTRIBUTING.md. The current references are both pull requests.

Comment thread tools/acrauth/acr.go Outdated
Comment thread tools/acrauth/go.sum
Comment thread tools/acrauth/authfile.go Outdated
The oc-mirror image authenticates to ACR by running 'az acr login' with DOCKER_COMMAND pointed at a shim script, so that az's internal 'docker login' call is intercepted and the credential can be scraped out of the argv. That coupling broke silently when azure-cli 2.88.0 switched to --password-stdin, and the shim's jq rewrite also clobbered the source-registry credentials that the init container had already written to the same auth file.

Do the exchange directly instead: trade the managed identity's Entra token for an ACR refresh token and upsert a single registry key into the auth file, leaving other registries and unmodelled fields intact.
…t be renamed over

A bind-mounted auth file cannot be replaced with rename(2) -- the kernel returns EBUSY. The oc-mirror container mounts auth.json directly, so the atomic write path fails there and the login aborts. Fall back to writing through the existing file in that case.
A non-nil pointer to an empty string would have been written out as a credential and reported as success -- the same silent-success failure this tool exists to remove.
os.WriteFile only applies its mode argument when it creates the file, so the fallback path left a pre-existing credential file at whatever mode it already had -- observed as 0644 in a container run. Chmod it explicitly, and stop claiming atomicity for a path that cannot have it.
@weherdh

Copy link
Copy Markdown
Contributor Author

PR-standard note: please link a Jira or GitHub tracking issue as required by CONTRIBUTING.md. The current references are both pull requests.

Updated that in the description: https://redhat.atlassian.net/browse/AROSLSRE-1819

containers/image prefers a non-empty identitytoken over auth and uses it for the OAuth refresh-token flow, so a stale token left on the entry we are replacing silently shadows the credential we just wrote -- oc-mirror would authenticate with the old value and fail. Delete it on the entry being replaced, while still preserving unrelated registries and their fields.

Also normalise the files to LF line endings so gofmt and gci pass, and tidy go.sum.
Copilot AI review requested due to automatic review settings September 1, 2026 05:45

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 8 out of 9 changed files in this pull request and generated 1 comment.

Comment thread tools/acrauth/authfile.go Outdated
os.WriteFile only applies its mode when it creates the file, so writing the refreshed token into an existing world-readable file exposed it until the follow-up chmod landed. Chmod first and treat ErrNotExist as fine, since WriteFile then creates the file at 0600.

Also commit the 'make tidy' output the verify job requires, including the go work sync results across the workspace.
Copilot AI review requested due to automatic review settings September 1, 2026 06:16

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 20 out of 33 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

tools/acrauth/options.go:80

  • --registry is only validated for non-empty, so values like https://myregistry.azurecr.io or myregistry.azurecr.io/some/path will be accepted and then mis-parsed in the token exchange, leading to confusing downstream errors. Consider rejecting URLs/paths early with a clearer validation error.
func (o *RawOptions) Validate() (*ValidatedOptions, error) {
	if o.Registry == "" {
		return nil, errors.New("--registry is required")
	}
	if o.AuthFile == "" {

tools/acrauth/command.go:31

  • The command currently accepts and ignores positional arguments. This can hide user mistakes (e.g., typos or copy/pasted extra args). If the command doesn't take args, set Args: cobra.NoArgs so Cobra will surface a clear error.
	cmd := &cobra.Command{
		Use:           "login",
		Short:         "Authenticate to an Azure Container Registry and record the credential in a container auth file",
		SilenceUsage:  true,
		SilenceErrors: true,

The tidy target is not idempotent in a single pass: 'go mod tidy' prunes each module's go.sum, then 'go work sync' adds back hashes the workspace build list needs. A second pass adds 48 go.sum lines that the first pass dropped, which is what the verify job was reporting.
Copilot AI review requested due to automatic review settings September 1, 2026 06:44

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 20 out of 33 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

tools/acrauth/authfile_test.go:112

  • readAuthFile explicitly treats a 0-byte auth file as empty input, but there is no test covering that behavior (only missing-file and malformed/valid JSON paths are exercised). Adding a test helps ensure regressions don’t reintroduce clobbering/parse failures for empty files.
func TestUpsertCredentialRejectsEmptyRegistry(t *testing.T) {
	path := filepath.Join(t.TempDir(), "auth.json")
	require.Error(t, UpsertCredential(path, "", NullGUIDUsername, "token"))
}

tools/acrauth/acr.go:82

  • for attempt := range retryAttempts does not compile because range can't be used over an int constant; this makes the retry helper unusable.
	for attempt := range retryAttempts {

@weherdh

Wenqi He (weherdh) commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Re-tested with a hostile fixture after the identitytoken catch. My original fixture used email as the representative unmodelled field, which is inert, so it could never have surfaced this.

Seeded the entry being replaced with a stale identity token, and an unrelated registry with its own:

Registry Seeded with
arohcpocpdev.azurecr.io auth (old) + identitytoken: STALE-TOKEN-MUST-BE-REMOVED
registry.redhat.io auth + email + identitytoken: KEEP-THIS-ONE

After acrauth login against the live dev ACR:

  • arohcpocpdev.azurecr.io → fields ["auth"], no identitytoken
  • registry.redhat.io → untouched, keeps email and its own token
  • mode 0600

Same fixture through the real container — oc-mirror image built with this binary, actual mirror, no --dry-run:

Success copying registry.access.redhat.com/ubi9/ubi-micro:latest -> arohcpocpdev.azurecr.io/ubi9/
 1 / 1 additional images mirrored successfully

With the previous code the stale token would have won and oc-mirror would have authenticated with the old credential. Re-read the auth file after the run to confirm the ACR entry still had auth only and the Red Hat entry was untouched.

Also in this push: LF line endings (gofmt -l clean) and go mod tidy (-diff now empty).

@raelga Rael Garcia (raelga) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

/lgtm
/approve

@openshift-ci

openshift-ci Bot commented Sep 3, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: raelga, weherdh

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved label Sep 3, 2026
@openshift-merge-bot
openshift-merge-bot Bot merged commit 2e1577a into Azure:main Sep 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants