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) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcp/internal/auth"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/runtime"
|
"git.wntrmute.dev/mc/mcp/internal/runtime"
|
||||||
"git.wntrmute.dev/mc/mcp/internal/servicedef"
|
"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)
|
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 {
|
for imageName, dockerfile := range def.Build.Images {
|
||||||
if imageFilter != "" && imageName != imageFilter {
|
if imageFilter != "" && imageName != imageFilter {
|
||||||
continue
|
continue
|
||||||
@@ -96,6 +108,19 @@ func findImageRef(def *servicedef.ServiceDef, imageName string) string {
|
|||||||
return ""
|
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.
|
// extractRepoName returns the repository name from an image reference.
|
||||||
// Examples:
|
// Examples:
|
||||||
//
|
//
|
||||||
@@ -124,6 +149,8 @@ func ensureImages(ctx context.Context, cfg *config.CLIConfig, def *servicedef.Se
|
|||||||
return nil // no build config, skip auto-build
|
return nil // no build config, skip auto-build
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registryLoginDone := false
|
||||||
|
|
||||||
for _, c := range def.Components {
|
for _, c := range def.Components {
|
||||||
if component != "" && c.Name != component {
|
if component != "" && c.Name != component {
|
||||||
continue
|
continue
|
||||||
@@ -153,6 +180,17 @@ func ensureImages(ctx context.Context, cfg *config.CLIConfig, def *servicedef.Se
|
|||||||
|
|
||||||
sourceDir := filepath.Join(cfg.Build.Workspace, def.Path)
|
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)
|
fmt.Printf("image %s not found, building from %s\n", c.Image, dockerfile)
|
||||||
if err := rt.Build(ctx, c.Image, sourceDir, dockerfile); err != nil {
|
if err := rt.Build(ctx, c.Image, sourceDir, dockerfile); err != nil {
|
||||||
return fmt.Errorf("auto-build %s: %w", c.Image, err)
|
return fmt.Errorf("auto-build %s: %w", c.Image, err)
|
||||||
|
|||||||
@@ -178,6 +178,18 @@ func (p *Podman) Inspect(ctx context.Context, name string) (ContainerInfo, error
|
|||||||
return info, nil
|
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.
|
// Build builds a container image from a Dockerfile.
|
||||||
func (p *Podman) Build(ctx context.Context, image, contextDir, dockerfile string) error {
|
func (p *Podman) Build(ctx context.Context, image, contextDir, dockerfile string) error {
|
||||||
args := []string{"build", "-t", image, "-f", dockerfile, contextDir}
|
args := []string{"build", "-t", image, "-f", dockerfile, contextDir}
|
||||||
|
|||||||
Reference in New Issue
Block a user