Fix container name handling for hyphenated service names
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>
This commit is contained in:
@@ -11,6 +11,8 @@ RestartSec=5
|
|||||||
|
|
||||||
User=mcp
|
User=mcp
|
||||||
Group=mcp
|
Group=mcp
|
||||||
|
Environment=HOME=/srv/mcp
|
||||||
|
Environment=XDG_RUNTIME_DIR=/run/user/%U
|
||||||
|
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (a *Agent) Deploy(ctx context.Context, req *mcpv1.DeployRequest) (*mcpv1.De
|
|||||||
// deployComponent handles the full deploy lifecycle for a single component.
|
// 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 {
|
func (a *Agent) deployComponent(ctx context.Context, serviceName string, cs *mcpv1.ComponentSpec, active bool) *mcpv1.ComponentResult {
|
||||||
compName := cs.GetName()
|
compName := cs.GetName()
|
||||||
containerName := serviceName + "-" + compName
|
containerName := ContainerNameFor(serviceName, compName)
|
||||||
|
|
||||||
desiredState := "running"
|
desiredState := "running"
|
||||||
if !active {
|
if !active {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (a *Agent) StopService(ctx context.Context, req *mcpv1.StopServiceRequest)
|
|||||||
|
|
||||||
var results []*mcpv1.ComponentResult
|
var results []*mcpv1.ComponentResult
|
||||||
for _, c := range components {
|
for _, c := range components {
|
||||||
containerName := req.GetName() + "-" + c.Name
|
containerName := ContainerNameFor(req.GetName(), c.Name)
|
||||||
r := &mcpv1.ComponentResult{Name: c.Name, Success: true}
|
r := &mcpv1.ComponentResult{Name: c.Name, Success: true}
|
||||||
|
|
||||||
if err := a.Runtime.Stop(ctx, containerName); err != nil {
|
if err := a.Runtime.Stop(ctx, containerName); err != nil {
|
||||||
@@ -94,7 +94,7 @@ func (a *Agent) RestartService(ctx context.Context, req *mcpv1.RestartServiceReq
|
|||||||
// startComponent removes any existing container and runs a fresh one from
|
// startComponent removes any existing container and runs a fresh one from
|
||||||
// the registry spec, then updates state to running.
|
// the registry spec, then updates state to running.
|
||||||
func startComponent(ctx context.Context, a *Agent, service string, c *registry.Component) *mcpv1.ComponentResult {
|
func startComponent(ctx context.Context, a *Agent, service string, c *registry.Component) *mcpv1.ComponentResult {
|
||||||
containerName := service + "-" + c.Name
|
containerName := ContainerNameFor(service, c.Name)
|
||||||
r := &mcpv1.ComponentResult{Name: c.Name, Success: true}
|
r := &mcpv1.ComponentResult{Name: c.Name, Success: true}
|
||||||
|
|
||||||
// Remove any pre-existing container; ignore errors for non-existent ones.
|
// Remove any pre-existing container; ignore errors for non-existent ones.
|
||||||
@@ -118,7 +118,7 @@ func startComponent(ctx context.Context, a *Agent, service string, c *registry.C
|
|||||||
// restartComponent stops, removes, and re-creates a container without
|
// restartComponent stops, removes, and re-creates a container without
|
||||||
// changing the desired_state in the registry.
|
// changing the desired_state in the registry.
|
||||||
func restartComponent(ctx context.Context, a *Agent, service string, c *registry.Component) *mcpv1.ComponentResult {
|
func restartComponent(ctx context.Context, a *Agent, service string, c *registry.Component) *mcpv1.ComponentResult {
|
||||||
containerName := service + "-" + c.Name
|
containerName := ContainerNameFor(service, c.Name)
|
||||||
r := &mcpv1.ComponentResult{Name: c.Name, Success: true}
|
r := &mcpv1.ComponentResult{Name: c.Name, Success: true}
|
||||||
|
|
||||||
_ = a.Runtime.Stop(ctx, containerName)
|
_ = a.Runtime.Stop(ctx, containerName)
|
||||||
@@ -142,7 +142,7 @@ func restartComponent(ctx context.Context, a *Agent, service string, c *registry
|
|||||||
// componentToSpec builds a runtime.ContainerSpec from a registry Component.
|
// componentToSpec builds a runtime.ContainerSpec from a registry Component.
|
||||||
func componentToSpec(service string, c *registry.Component) runtime.ContainerSpec {
|
func componentToSpec(service string, c *registry.Component) runtime.ContainerSpec {
|
||||||
return runtime.ContainerSpec{
|
return runtime.ContainerSpec{
|
||||||
Name: service + "-" + c.Name,
|
Name: ContainerNameFor(service, c.Name),
|
||||||
Image: c.Image,
|
Image: c.Image,
|
||||||
Network: c.Network,
|
Network: c.Network,
|
||||||
User: c.UserSpec,
|
User: c.UserSpec,
|
||||||
|
|||||||
34
internal/agent/names.go
Normal file
34
internal/agent/names.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ContainerNameFor returns the expected container name for a service and
|
||||||
|
// component. For single-component services where the component name equals
|
||||||
|
// the service name, the container name is just the service name (e.g.,
|
||||||
|
// "mc-proxy" not "mc-proxy-mc-proxy").
|
||||||
|
func ContainerNameFor(service, component string) string {
|
||||||
|
if service == component {
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
return service + "-" + component
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitContainerName splits a container name into service and component parts.
|
||||||
|
// It checks known service names first to handle names like "mc-proxy" where a
|
||||||
|
// naive split on "-" would produce the wrong result. If no known service
|
||||||
|
// matches, it falls back to splitting on the first "-".
|
||||||
|
func SplitContainerName(name string, knownServices map[string]bool) (service, component string) {
|
||||||
|
if knownServices[name] {
|
||||||
|
return name, name
|
||||||
|
}
|
||||||
|
for svc := range knownServices {
|
||||||
|
prefix := svc + "-"
|
||||||
|
if strings.HasPrefix(name, prefix) && len(name) > len(prefix) {
|
||||||
|
return svc, name[len(prefix):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i := strings.Index(name, "-"); i >= 0 {
|
||||||
|
return name[:i], name[i+1:]
|
||||||
|
}
|
||||||
|
return name, name
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package agent
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
mcpv1 "git.wntrmute.dev/kyle/mcp/gen/mcp/v1"
|
mcpv1 "git.wntrmute.dev/kyle/mcp/gen/mcp/v1"
|
||||||
@@ -75,7 +74,10 @@ func (a *Agent) liveCheckServices(ctx context.Context) ([]*mcpv1.ServiceInfo, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
var result []*mcpv1.ServiceInfo
|
var result []*mcpv1.ServiceInfo
|
||||||
|
knownServices := make(map[string]bool, len(services))
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
|
knownServices[svc.Name] = true
|
||||||
|
|
||||||
components, err := registry.ListComponents(a.DB, svc.Name)
|
components, err := registry.ListComponents(a.DB, svc.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list components for %q: %w", svc.Name, err)
|
return nil, fmt.Errorf("list components for %q: %w", svc.Name, err)
|
||||||
@@ -87,7 +89,7 @@ func (a *Agent) liveCheckServices(ctx context.Context) ([]*mcpv1.ServiceInfo, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, comp := range components {
|
for _, comp := range components {
|
||||||
containerName := svc.Name + "-" + comp.Name
|
containerName := ContainerNameFor(svc.Name, comp.Name)
|
||||||
ci := &mcpv1.ComponentInfo{
|
ci := &mcpv1.ComponentInfo{
|
||||||
Name: comp.Name,
|
Name: comp.Name,
|
||||||
Image: comp.Image,
|
Image: comp.Image,
|
||||||
@@ -116,7 +118,7 @@ func (a *Agent) liveCheckServices(ctx context.Context) ([]*mcpv1.ServiceInfo, er
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
svcName, compName := splitContainerName(c.Name)
|
svcName, compName := SplitContainerName(c.Name, knownServices)
|
||||||
|
|
||||||
result = append(result, &mcpv1.ServiceInfo{
|
result = append(result, &mcpv1.ServiceInfo{
|
||||||
Name: svcName,
|
Name: svcName,
|
||||||
@@ -210,13 +212,3 @@ func (a *Agent) GetServiceStatus(ctx context.Context, req *mcpv1.GetServiceStatu
|
|||||||
RecentEvents: protoEvents,
|
RecentEvents: protoEvents,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitContainerName splits a container name like "metacrypt-api" into service
|
|
||||||
// and component parts. If there is no hyphen, the whole name is used as both
|
|
||||||
// the service and component name.
|
|
||||||
func splitContainerName(name string) (service, component string) {
|
|
||||||
if i := strings.Index(name, "-"); i >= 0 {
|
|
||||||
return name[:i], name[i+1:]
|
|
||||||
}
|
|
||||||
return name, name
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -253,22 +253,47 @@ func TestGetServiceStatus_IgnoreSkipsDrift(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSplitContainerName(t *testing.T) {
|
func TestSplitContainerName(t *testing.T) {
|
||||||
|
known := map[string]bool{
|
||||||
|
"metacrypt": true,
|
||||||
|
"mc-proxy": true,
|
||||||
|
"mcr": true,
|
||||||
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
service string
|
service string
|
||||||
comp string
|
comp string
|
||||||
}{
|
}{
|
||||||
{"metacrypt-api", "metacrypt", "api"},
|
{"metacrypt-api", "metacrypt", "api"},
|
||||||
{"metacrypt-web-ui", "metacrypt", "web-ui"},
|
{"metacrypt-web", "metacrypt", "web"},
|
||||||
|
{"mc-proxy", "mc-proxy", "mc-proxy"},
|
||||||
|
{"mcr-api", "mcr", "api"},
|
||||||
{"standalone", "standalone", "standalone"},
|
{"standalone", "standalone", "standalone"},
|
||||||
|
{"unknown-thing", "unknown", "thing"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
svc, comp := splitContainerName(tt.name)
|
svc, comp := SplitContainerName(tt.name, known)
|
||||||
if svc != tt.service || comp != tt.comp {
|
if svc != tt.service || comp != tt.comp {
|
||||||
t.Fatalf("splitContainerName(%q) = (%q, %q), want (%q, %q)",
|
t.Fatalf("SplitContainerName(%q) = (%q, %q), want (%q, %q)",
|
||||||
tt.name, svc, comp, tt.service, tt.comp)
|
tt.name, svc, comp, tt.service, tt.comp)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainerNameFor(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
service, component, want string
|
||||||
|
}{
|
||||||
|
{"metacrypt", "api", "metacrypt-api"},
|
||||||
|
{"mc-proxy", "mc-proxy", "mc-proxy"},
|
||||||
|
{"mcr", "web", "mcr-web"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := ContainerNameFor(tt.service, tt.component)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("ContainerNameFor(%q, %q) = %q, want %q",
|
||||||
|
tt.service, tt.component, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ type podmanPSEntry struct {
|
|||||||
Names []string `json:"Names"`
|
Names []string `json:"Names"`
|
||||||
Image string `json:"Image"`
|
Image string `json:"Image"`
|
||||||
State string `json:"State"`
|
State string `json:"State"`
|
||||||
Command string `json:"Command"`
|
Command []string `json:"Command"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns information about all containers.
|
// List returns information about all containers.
|
||||||
|
|||||||
Reference in New Issue
Block a user