All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0, mc-proxy to v1.1.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
5.0 KiB
Go
169 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
|
"git.wntrmute.dev/mc/mcp/internal/runtime"
|
|
"git.wntrmute.dev/mc/mcp/internal/servicedef"
|
|
)
|
|
|
|
func buildCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "build <service>[/<image>]",
|
|
Short: "Build and push images for a service",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := config.LoadCLIConfig(cfgPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load config: %w", err)
|
|
}
|
|
|
|
serviceName, imageFilter := parseServiceArg(args[0])
|
|
|
|
def, err := loadServiceDef(cmd, cfg, serviceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rt := &runtime.Podman{}
|
|
return buildServiceImages(cmd.Context(), cfg, def, rt, imageFilter)
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildServiceImages builds and pushes images for a service definition.
|
|
// If imageFilter is non-empty, only the matching image is built.
|
|
func buildServiceImages(ctx context.Context, cfg *config.CLIConfig, def *servicedef.ServiceDef, rt *runtime.Podman, imageFilter string) error {
|
|
if def.Build == nil || len(def.Build.Images) == 0 {
|
|
return fmt.Errorf("service %q has no [build.images] configuration", def.Name)
|
|
}
|
|
if def.Path == "" {
|
|
return fmt.Errorf("service %q has no path configured", def.Name)
|
|
}
|
|
if cfg.Build.Workspace == "" {
|
|
return fmt.Errorf("build.workspace is not configured in %s", cfgPath)
|
|
}
|
|
|
|
sourceDir := filepath.Join(cfg.Build.Workspace, def.Path)
|
|
|
|
for imageName, dockerfile := range def.Build.Images {
|
|
if imageFilter != "" && imageName != imageFilter {
|
|
continue
|
|
}
|
|
|
|
imageRef := findImageRef(def, imageName)
|
|
if imageRef == "" {
|
|
return fmt.Errorf("no component references image %q in service %q", imageName, def.Name)
|
|
}
|
|
|
|
fmt.Printf("building %s from %s\n", imageRef, dockerfile)
|
|
if err := rt.Build(ctx, imageRef, sourceDir, dockerfile); err != nil {
|
|
return fmt.Errorf("build %s: %w", imageRef, err)
|
|
}
|
|
|
|
fmt.Printf("pushing %s\n", imageRef)
|
|
if err := rt.Push(ctx, imageRef); err != nil {
|
|
return fmt.Errorf("push %s: %w", imageRef, err)
|
|
}
|
|
}
|
|
|
|
if imageFilter != "" {
|
|
if _, ok := def.Build.Images[imageFilter]; !ok {
|
|
return fmt.Errorf("image %q not found in [build.images] for service %q", imageFilter, def.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findImageRef finds the full image reference for a build image name by
|
|
// matching it against component image fields. The image name from
|
|
// [build.images] matches the repository name in the component's image
|
|
// reference (the path segment after the last slash, before the tag).
|
|
func findImageRef(def *servicedef.ServiceDef, imageName string) string {
|
|
for _, c := range def.Components {
|
|
repoName := extractRepoName(c.Image)
|
|
if repoName == imageName {
|
|
return c.Image
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// extractRepoName returns the repository name from an image reference.
|
|
// Examples:
|
|
//
|
|
// "mcr.svc.mcp.metacircular.net:8443/mcr:v1.1.0" -> "mcr"
|
|
// "mcr.svc.mcp.metacircular.net:8443/mcr-web:v1.2.0" -> "mcr-web"
|
|
// "mcr-web:v1.2.0" -> "mcr-web"
|
|
// "mcr-web" -> "mcr-web"
|
|
func extractRepoName(image string) string {
|
|
// Strip registry prefix (everything up to and including the last slash).
|
|
name := image
|
|
if i := strings.LastIndex(image, "/"); i >= 0 {
|
|
name = image[i+1:]
|
|
}
|
|
// Strip tag.
|
|
if i := strings.LastIndex(name, ":"); i >= 0 {
|
|
name = name[:i]
|
|
}
|
|
return name
|
|
}
|
|
|
|
// ensureImages checks that all component images exist in the registry.
|
|
// If an image is missing and the service has build configuration, it
|
|
// builds and pushes the image. Returns nil if all images are available.
|
|
func ensureImages(ctx context.Context, cfg *config.CLIConfig, def *servicedef.ServiceDef, rt *runtime.Podman, component string) error {
|
|
if def.Build == nil || len(def.Build.Images) == 0 {
|
|
return nil // no build config, skip auto-build
|
|
}
|
|
|
|
for _, c := range def.Components {
|
|
if component != "" && c.Name != component {
|
|
continue
|
|
}
|
|
|
|
repoName := extractRepoName(c.Image)
|
|
dockerfile, ok := def.Build.Images[repoName]
|
|
if !ok {
|
|
continue // no Dockerfile for this image, skip
|
|
}
|
|
|
|
exists, err := rt.ImageExists(ctx, c.Image)
|
|
if err != nil {
|
|
return fmt.Errorf("check image %s: %w", c.Image, err)
|
|
}
|
|
if exists {
|
|
continue
|
|
}
|
|
|
|
// Image missing — build and push.
|
|
if def.Path == "" {
|
|
return fmt.Errorf("image %s not found in registry and service %q has no path configured", c.Image, def.Name)
|
|
}
|
|
if cfg.Build.Workspace == "" {
|
|
return fmt.Errorf("image %s not found in registry and build.workspace is not configured", c.Image)
|
|
}
|
|
|
|
sourceDir := filepath.Join(cfg.Build.Workspace, def.Path)
|
|
|
|
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)
|
|
}
|
|
|
|
fmt.Printf("pushing %s\n", c.Image)
|
|
if err := rt.Push(ctx, c.Image); err != nil {
|
|
return fmt.Errorf("auto-push %s: %w", c.Image, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|