Extract ContainerNameFor and SplitContainerName into names.go. ContainerNameFor handles single-component services where service name equals component name (e.g., mc-proxy → "mc-proxy" not "mc-proxy-mc-proxy"). SplitContainerName checks known services from the registry before falling back to naive split on "-", fixing mc-proxy being misidentified as service "mc" component "proxy". Also fixes podman ps JSON parsing (Command field is []string not string) found during deployment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
146 lines
4.1 KiB
Go
146 lines
4.1 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
mcpv1 "git.wntrmute.dev/kyle/mcp/gen/mcp/v1"
|
|
"git.wntrmute.dev/kyle/mcp/internal/registry"
|
|
"git.wntrmute.dev/kyle/mcp/internal/runtime"
|
|
)
|
|
|
|
// Deploy deploys a service (or a single component of it) to this node.
|
|
func (a *Agent) Deploy(ctx context.Context, req *mcpv1.DeployRequest) (*mcpv1.DeployResponse, error) {
|
|
spec := req.GetService()
|
|
if spec == nil {
|
|
return nil, fmt.Errorf("deploy: missing service spec")
|
|
}
|
|
|
|
serviceName := spec.GetName()
|
|
a.Logger.Info("deploying", "service", serviceName)
|
|
|
|
if err := ensureService(a.DB, serviceName, spec.GetActive()); err != nil {
|
|
return nil, fmt.Errorf("deploy: ensure service %q: %w", serviceName, err)
|
|
}
|
|
|
|
components := spec.GetComponents()
|
|
if target := req.GetComponent(); target != "" {
|
|
var filtered []*mcpv1.ComponentSpec
|
|
for _, cs := range components {
|
|
if cs.GetName() == target {
|
|
filtered = append(filtered, cs)
|
|
}
|
|
}
|
|
components = filtered
|
|
}
|
|
|
|
var results []*mcpv1.ComponentResult
|
|
active := spec.GetActive()
|
|
for _, cs := range components {
|
|
result := a.deployComponent(ctx, serviceName, cs, active)
|
|
results = append(results, result)
|
|
}
|
|
|
|
return &mcpv1.DeployResponse{Results: results}, nil
|
|
}
|
|
|
|
// deployComponent handles the full deploy lifecycle for a single component.
|
|
func (a *Agent) deployComponent(ctx context.Context, serviceName string, cs *mcpv1.ComponentSpec, active bool) *mcpv1.ComponentResult {
|
|
compName := cs.GetName()
|
|
containerName := ContainerNameFor(serviceName, compName)
|
|
|
|
desiredState := "running"
|
|
if !active {
|
|
desiredState = "stopped"
|
|
}
|
|
|
|
a.Logger.Info("deploying component", "service", serviceName, "component", compName, "desired", desiredState)
|
|
|
|
regComp := ®istry.Component{
|
|
Name: compName,
|
|
Service: serviceName,
|
|
Image: cs.GetImage(),
|
|
Network: cs.GetNetwork(),
|
|
UserSpec: cs.GetUser(),
|
|
Restart: cs.GetRestart(),
|
|
DesiredState: desiredState,
|
|
Version: runtime.ExtractVersion(cs.GetImage()),
|
|
Ports: cs.GetPorts(),
|
|
Volumes: cs.GetVolumes(),
|
|
Cmd: cs.GetCmd(),
|
|
}
|
|
|
|
if err := ensureComponent(a.DB, regComp); err != nil {
|
|
return &mcpv1.ComponentResult{
|
|
Name: compName,
|
|
Error: fmt.Sprintf("ensure component: %v", err),
|
|
}
|
|
}
|
|
|
|
if err := a.Runtime.Pull(ctx, cs.GetImage()); err != nil {
|
|
return &mcpv1.ComponentResult{
|
|
Name: compName,
|
|
Error: fmt.Sprintf("pull image: %v", err),
|
|
}
|
|
}
|
|
|
|
_ = a.Runtime.Stop(ctx, containerName) // may not exist yet
|
|
_ = a.Runtime.Remove(ctx, containerName) // may not exist yet
|
|
|
|
runSpec := runtime.ContainerSpec{
|
|
Name: containerName,
|
|
Image: cs.GetImage(),
|
|
Network: cs.GetNetwork(),
|
|
User: cs.GetUser(),
|
|
Restart: cs.GetRestart(),
|
|
Ports: cs.GetPorts(),
|
|
Volumes: cs.GetVolumes(),
|
|
Cmd: cs.GetCmd(),
|
|
}
|
|
if err := a.Runtime.Run(ctx, runSpec); err != nil {
|
|
_ = registry.UpdateComponentState(a.DB, serviceName, compName, "", "removed")
|
|
return &mcpv1.ComponentResult{
|
|
Name: compName,
|
|
Error: fmt.Sprintf("run container: %v", err),
|
|
}
|
|
}
|
|
|
|
if err := registry.UpdateComponentState(a.DB, serviceName, compName, "running", "running"); err != nil {
|
|
a.Logger.Warn("failed to update component state", "service", serviceName, "component", compName, "err", err)
|
|
}
|
|
|
|
return &mcpv1.ComponentResult{
|
|
Name: compName,
|
|
Success: true,
|
|
}
|
|
}
|
|
|
|
// ensureService creates the service if it does not exist, or updates its
|
|
// active flag if it does.
|
|
func ensureService(db *sql.DB, name string, active bool) error {
|
|
_, err := registry.GetService(db, name)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return registry.CreateService(db, name, active)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return registry.UpdateServiceActive(db, name, active)
|
|
}
|
|
|
|
// ensureComponent creates the component if it does not exist, or updates its
|
|
// spec if it does.
|
|
func ensureComponent(db *sql.DB, c *registry.Component) error {
|
|
_, err := registry.GetComponent(db, c.Service, c.Name)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
c.ObservedState = "unknown"
|
|
return registry.CreateComponent(db, c)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return registry.UpdateComponentSpec(db, c)
|
|
}
|