From dd167b8e0ba7a0b4e7ff23015c35d4916548458e Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 28 Mar 2026 15:13:35 -0700 Subject: [PATCH] Auto-login to MCR before image push using CLI token mcp build and mcp deploy (auto-build path) now authenticate to the container registry using the CLI's stored MCIAS token before pushing. MCR accepts JWTs as passwords, so this works with both human and service account tokens. Falls back silently to existing podman auth. Eliminates the need for a separate interactive `podman login` step. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/mcp/build.go | 38 ++++++++++++++++++++++++++++++++++++++ internal/runtime/podman.go | 12 ++++++++++++ 2 files changed, 50 insertions(+) diff --git a/cmd/mcp/build.go b/cmd/mcp/build.go index 4060c24..e8b47aa 100644 --- a/cmd/mcp/build.go +++ b/cmd/mcp/build.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" + "git.wntrmute.dev/mc/mcp/internal/auth" "git.wntrmute.dev/mc/mcp/internal/config" "git.wntrmute.dev/mc/mcp/internal/runtime" "git.wntrmute.dev/mc/mcp/internal/servicedef" @@ -52,6 +53,17 @@ func buildServiceImages(ctx context.Context, cfg *config.CLIConfig, def *service sourceDir := filepath.Join(cfg.Build.Workspace, def.Path) + // Auto-login to the registry using the CLI's stored MCIAS token. + // MCR accepts JWTs as passwords, so this works for both human and + // service account tokens. Failures are non-fatal — existing podman + // auth may suffice. + if token, err := auth.LoadToken(cfg.Auth.TokenPath); err == nil && token != "" { + registry := extractRegistry(def) + if registry != "" { + _ = rt.Login(ctx, registry, "mcp", token) + } + } + for imageName, dockerfile := range def.Build.Images { if imageFilter != "" && imageName != imageFilter { continue @@ -96,6 +108,19 @@ func findImageRef(def *servicedef.ServiceDef, imageName string) string { return "" } +// extractRegistry returns the registry host from the first component's +// image reference (e.g., "mcr.svc.mcp.metacircular.net:8443" from +// "mcr.svc.mcp.metacircular.net:8443/mcq:v0.1.1"). Returns empty +// string if no slash is found. +func extractRegistry(def *servicedef.ServiceDef) string { + for _, c := range def.Components { + if i := strings.LastIndex(c.Image, "/"); i > 0 { + return c.Image[:i] + } + } + return "" +} + // extractRepoName returns the repository name from an image reference. // Examples: // @@ -124,6 +149,8 @@ func ensureImages(ctx context.Context, cfg *config.CLIConfig, def *servicedef.Se return nil // no build config, skip auto-build } + registryLoginDone := false + for _, c := range def.Components { if component != "" && c.Name != component { continue @@ -153,6 +180,17 @@ func ensureImages(ctx context.Context, cfg *config.CLIConfig, def *servicedef.Se sourceDir := filepath.Join(cfg.Build.Workspace, def.Path) + // Auto-login to registry before first push. + if !registryLoginDone { + if token, err := auth.LoadToken(cfg.Auth.TokenPath); err == nil && token != "" { + registry := extractRegistry(def) + if registry != "" { + _ = rt.Login(ctx, registry, "mcp", token) + } + } + registryLoginDone = true + } + fmt.Printf("image %s not found, building from %s\n", c.Image, dockerfile) if err := rt.Build(ctx, c.Image, sourceDir, dockerfile); err != nil { return fmt.Errorf("auto-build %s: %w", c.Image, err) diff --git a/internal/runtime/podman.go b/internal/runtime/podman.go index 846a5b4..9f41cec 100644 --- a/internal/runtime/podman.go +++ b/internal/runtime/podman.go @@ -178,6 +178,18 @@ func (p *Podman) Inspect(ctx context.Context, name string) (ContainerInfo, error return info, nil } +// Login authenticates to a container registry using the given token as +// the password. This enables non-interactive push with service account +// tokens (MCR accepts MCIAS JWTs as passwords). +func (p *Podman) Login(ctx context.Context, registry, username, token string) error { + cmd := exec.CommandContext(ctx, p.command(), "login", "--username", username, "--password-stdin", registry) //nolint:gosec // args built programmatically + cmd.Stdin = strings.NewReader(token) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("podman login %q: %w: %s", registry, err, out) + } + return nil +} + // Build builds a container image from a Dockerfile. func (p *Podman) Build(ctx context.Context, image, contextDir, dockerfile string) error { args := []string{"build", "-t", image, "-f", dockerfile, contextDir}