Add mcp build command and deploy auto-build
Extends MCP to own the full build-push-deploy lifecycle. When deploying, the CLI checks whether each component's image tag exists in the registry and builds/pushes automatically if missing and build config is present. - Add Build, Push, ImageExists to runtime.Runtime interface (podman impl) - Add mcp build <service>[/<image>] command - Add [build] section to CLI config (workspace path) - Add path and [build.images] to service definitions - Wire auto-build into mcp deploy before agent RPC - Update ARCHITECTURE.md with runtime interface and deploy auto-build docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,10 @@ func (f *fakeRuntime) Pull(_ context.Context, _ string) error { re
|
||||
func (f *fakeRuntime) Run(_ context.Context, _ runtime.ContainerSpec) error { return nil }
|
||||
func (f *fakeRuntime) Stop(_ context.Context, _ string) error { return nil }
|
||||
func (f *fakeRuntime) Remove(_ context.Context, _ string) error { return nil }
|
||||
func (f *fakeRuntime) Build(_ context.Context, _, _, _ string) error { return nil }
|
||||
func (f *fakeRuntime) Push(_ context.Context, _ string) error { return nil }
|
||||
|
||||
func (f *fakeRuntime) ImageExists(_ context.Context, _ string) (bool, error) { return true, nil }
|
||||
|
||||
func (f *fakeRuntime) List(_ context.Context) ([]runtime.ContainerInfo, error) {
|
||||
return f.containers, f.listErr
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
@@ -10,11 +11,17 @@ import (
|
||||
// CLIConfig is the configuration for the mcp CLI binary.
|
||||
type CLIConfig struct {
|
||||
Services ServicesConfig `toml:"services"`
|
||||
Build BuildConfig `toml:"build"`
|
||||
MCIAS MCIASConfig `toml:"mcias"`
|
||||
Auth AuthConfig `toml:"auth"`
|
||||
Nodes []NodeConfig `toml:"nodes"`
|
||||
}
|
||||
|
||||
// BuildConfig holds settings for building container images.
|
||||
type BuildConfig struct {
|
||||
Workspace string `toml:"workspace"`
|
||||
}
|
||||
|
||||
// ServicesConfig defines where service definition files live.
|
||||
type ServicesConfig struct {
|
||||
Dir string `toml:"dir"`
|
||||
@@ -66,6 +73,9 @@ func applyCLIEnvOverrides(cfg *CLIConfig) {
|
||||
if v := os.Getenv("MCP_SERVICES_DIR"); v != "" {
|
||||
cfg.Services.Dir = v
|
||||
}
|
||||
if v := os.Getenv("MCP_BUILD_WORKSPACE"); v != "" {
|
||||
cfg.Build.Workspace = v
|
||||
}
|
||||
if v := os.Getenv("MCP_MCIAS_SERVER_URL"); v != "" {
|
||||
cfg.MCIAS.ServerURL = v
|
||||
}
|
||||
@@ -93,5 +103,15 @@ func validateCLIConfig(cfg *CLIConfig) error {
|
||||
if cfg.Auth.TokenPath == "" {
|
||||
return fmt.Errorf("auth.token_path is required")
|
||||
}
|
||||
|
||||
// Expand ~ in workspace path.
|
||||
if strings.HasPrefix(cfg.Build.Workspace, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("expand workspace path: %w", err)
|
||||
}
|
||||
cfg.Build.Workspace = home + cfg.Build.Workspace[1:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ func (f *fakeRuntime) Pull(_ context.Context, _ string) error { re
|
||||
func (f *fakeRuntime) Run(_ context.Context, _ runtime.ContainerSpec) error { return nil }
|
||||
func (f *fakeRuntime) Stop(_ context.Context, _ string) error { return nil }
|
||||
func (f *fakeRuntime) Remove(_ context.Context, _ string) error { return nil }
|
||||
func (f *fakeRuntime) Build(_ context.Context, _, _, _ string) error { return nil }
|
||||
func (f *fakeRuntime) Push(_ context.Context, _ string) error { return nil }
|
||||
|
||||
func (f *fakeRuntime) ImageExists(_ context.Context, _ string) (bool, error) { return true, nil }
|
||||
|
||||
func (f *fakeRuntime) Inspect(_ context.Context, _ string) (runtime.ContainerInfo, error) {
|
||||
return runtime.ContainerInfo{}, nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package runtime
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -177,6 +178,40 @@ func (p *Podman) Inspect(ctx context.Context, name string) (ContainerInfo, error
|
||||
return info, 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}
|
||||
cmd := exec.CommandContext(ctx, p.command(), args...) //nolint:gosec // args built programmatically
|
||||
cmd.Dir = contextDir
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("podman build %q: %w: %s", image, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Push pushes a container image to a remote registry.
|
||||
func (p *Podman) Push(ctx context.Context, image string) error {
|
||||
cmd := exec.CommandContext(ctx, p.command(), "push", image) //nolint:gosec // args built programmatically
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("podman push %q: %w: %s", image, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImageExists checks whether an image tag exists in a remote registry.
|
||||
func (p *Podman) ImageExists(ctx context.Context, image string) (bool, error) {
|
||||
cmd := exec.CommandContext(ctx, p.command(), "manifest", "inspect", "docker://"+image) //nolint:gosec // args built programmatically
|
||||
if err := cmd.Run(); err != nil {
|
||||
// Exit code 1 means the manifest was not found.
|
||||
var exitErr *exec.ExitError
|
||||
if ok := errors.As(err, &exitErr); ok && exitErr.ExitCode() == 1 {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("podman manifest inspect %q: %w", image, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// podmanPSEntry is a single entry from podman ps --format json.
|
||||
type podmanPSEntry struct {
|
||||
Names []string `json:"Names"`
|
||||
|
||||
@@ -34,7 +34,9 @@ type ContainerInfo struct {
|
||||
Started time.Time // when the container started (zero if not running)
|
||||
}
|
||||
|
||||
// Runtime is the container runtime abstraction.
|
||||
// Runtime is the container runtime abstraction. The first six methods are
|
||||
// used by the agent for container lifecycle. The last three are used by the
|
||||
// CLI for building and pushing images.
|
||||
type Runtime interface {
|
||||
Pull(ctx context.Context, image string) error
|
||||
Run(ctx context.Context, spec ContainerSpec) error
|
||||
@@ -42,6 +44,10 @@ type Runtime interface {
|
||||
Remove(ctx context.Context, name string) error
|
||||
Inspect(ctx context.Context, name string) (ContainerInfo, error)
|
||||
List(ctx context.Context) ([]ContainerInfo, error)
|
||||
|
||||
Build(ctx context.Context, image, contextDir, dockerfile string) error
|
||||
Push(ctx context.Context, image string) error
|
||||
ImageExists(ctx context.Context, image string) (bool, error)
|
||||
}
|
||||
|
||||
// ExtractVersion parses the tag from an image reference.
|
||||
|
||||
@@ -18,9 +18,17 @@ type ServiceDef struct {
|
||||
Name string `toml:"name"`
|
||||
Node string `toml:"node"`
|
||||
Active *bool `toml:"active,omitempty"`
|
||||
Path string `toml:"path,omitempty"`
|
||||
Build *BuildDef `toml:"build,omitempty"`
|
||||
Components []ComponentDef `toml:"components"`
|
||||
}
|
||||
|
||||
// BuildDef describes how to build container images for a service.
|
||||
type BuildDef struct {
|
||||
Images map[string]string `toml:"images"`
|
||||
UsesMCDSL bool `toml:"uses_mcdsl,omitempty"`
|
||||
}
|
||||
|
||||
// RouteDef describes a route for a component, used for automatic port
|
||||
// allocation and mc-proxy integration.
|
||||
type RouteDef struct {
|
||||
|
||||
Reference in New Issue
Block a user