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}