Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da59d60c2d | |||
| 598ea44e0b | |||
| 6fd81cacf2 | |||
| 20735e4b41 | |||
| 3c0b55f9f8 | |||
| 78890ed76a | |||
| c5ff5bb63c | |||
| ddd6f123ab | |||
| 90445507a3 | |||
| 68d670b3ed | |||
| 714320c018 | |||
| fa8ba6fac1 | |||
| f66758b92b | |||
| 09d0d197c3 | |||
| 52914d50b0 | |||
| bb4bee51ba | |||
| 4ac8a6d60b | |||
| d8f45ca520 |
17
CLAUDE.md
17
CLAUDE.md
@@ -12,6 +12,21 @@ MCP has two components:
|
||||
|
||||
Services have one or more components (containers). Container naming: `<service>-<component>`.
|
||||
|
||||
## v2 Development (Multi-Node)
|
||||
|
||||
MCP v2 extends the single-node agent model to a multi-node fleet with a central master process. See the root repo's `docs/phase-e-plan.md` and `docs/architecture-v2.md` for the full design.
|
||||
|
||||
**Current state:**
|
||||
- **svc** is operational as an edge node (manages mc-proxy routing only, no containers)
|
||||
- **rift** runs the agent with full container management
|
||||
- **orion** is provisioned but offline for maintenance
|
||||
|
||||
**Key v2 concepts (in development):**
|
||||
- **mcp-master** — central orchestrator on rift. Accepts CLI commands, dispatches to agents, maintains node registry, coordinates edge routing.
|
||||
- **Agent self-registration** — agents register with the master on startup (name, role, address, arch). No static node config required after bootstrap.
|
||||
- **Tier-based placement** — `tier = "core"` runs on the master node, `tier = "worker"` (default) is auto-placed on a worker with capacity, `node = "<name>"` overrides for pinned services.
|
||||
- **Edge routing** — `public = true` on routes declares intent; the master assigns the route to an edge node (currently svc).
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
@@ -33,7 +48,7 @@ Run a single test: `go test ./internal/registry/ -run TestComponentCRUD`
|
||||
|
||||
- `cmd/mcp/` — CLI entry point
|
||||
- `cmd/mcp-agent/` — Agent entry point
|
||||
- `internal/agent/` — Agent core (deploy, lifecycle, sync, adopt, status, files)
|
||||
- `internal/agent/` — Agent core (deploy, lifecycle, sync, adopt, status, files, edge_rpc, dns, proxy, certs)
|
||||
- `internal/runtime/` — Container runtime abstraction (podman)
|
||||
- `internal/registry/` — SQLite registry (services, components, events)
|
||||
- `internal/monitor/` — Monitoring subsystem (watch loop, alerting)
|
||||
|
||||
7
Makefile
7
Makefile
@@ -8,6 +8,9 @@ mcp:
|
||||
mcp-agent:
|
||||
CGO_ENABLED=0 go build $(LDFLAGS) -o mcp-agent ./cmd/mcp-agent
|
||||
|
||||
mcp-master:
|
||||
CGO_ENABLED=0 go build $(LDFLAGS) -o mcp-master ./cmd/mcp-master
|
||||
|
||||
build:
|
||||
go build ./...
|
||||
|
||||
@@ -30,6 +33,6 @@ proto-lint:
|
||||
buf breaking --against '.git#branch=master,subdir=proto'
|
||||
|
||||
clean:
|
||||
rm -f mcp mcp-agent
|
||||
rm -f mcp mcp-agent mcp-master
|
||||
|
||||
all: vet lint test mcp mcp-agent
|
||||
all: vet lint test mcp mcp-agent mcp-master
|
||||
|
||||
49
cmd/mcp-master/main.go
Normal file
49
cmd/mcp-master/main.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||
"git.wntrmute.dev/mc/mcp/internal/master"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
cfgPath string
|
||||
)
|
||||
|
||||
func main() {
|
||||
root := &cobra.Command{
|
||||
Use: "mcp-master",
|
||||
Short: "Metacircular Control Plane master",
|
||||
}
|
||||
root.PersistentFlags().StringVarP(&cfgPath, "config", "c", "", "config file path")
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(version)
|
||||
},
|
||||
})
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Start the master server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadMasterConfig(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
return master.Run(cfg, version)
|
||||
},
|
||||
})
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
)
|
||||
|
||||
func deployCmd() *cobra.Command {
|
||||
var direct bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy <service>[/<component>]",
|
||||
Short: "Deploy service from service definition",
|
||||
@@ -40,6 +42,12 @@ func deployCmd() *cobra.Command {
|
||||
|
||||
spec := servicedef.ToProto(def)
|
||||
|
||||
// Route through master if configured and not in direct mode.
|
||||
if cfg.Master != nil && cfg.Master.Address != "" && !direct {
|
||||
return deployViaMaster(cfg, spec)
|
||||
}
|
||||
|
||||
// Direct mode: deploy to agent.
|
||||
address, err := findNodeAddress(cfg, def.Node)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,9 +72,48 @@ func deployCmd() *cobra.Command {
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringP("file", "f", "", "service definition file")
|
||||
cmd.Flags().BoolVar(&direct, "direct", false, "bypass master, deploy directly to agent (v1 mode)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deployViaMaster(cfg *config.CLIConfig, spec *mcpv1.ServiceSpec) error {
|
||||
client, conn, err := dialMaster(cfg.Master.Address, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial master: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
resp, err := client.Deploy(context.Background(), &mcpv1.MasterDeployRequest{
|
||||
Service: spec,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("master deploy: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" %s: placed on %s\n", spec.GetName(), resp.GetNode())
|
||||
if r := resp.GetDeployResult(); r != nil {
|
||||
printStepResult("deploy", r)
|
||||
}
|
||||
if r := resp.GetDnsResult(); r != nil {
|
||||
printStepResult("dns", r)
|
||||
}
|
||||
if r := resp.GetEdgeRouteResult(); r != nil {
|
||||
printStepResult("edge", r)
|
||||
}
|
||||
if !resp.GetSuccess() {
|
||||
return fmt.Errorf("deploy failed: %s", resp.GetError())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printStepResult(name string, r *mcpv1.StepResult) {
|
||||
if r.GetSuccess() {
|
||||
fmt.Printf(" %s: ok\n", name)
|
||||
} else {
|
||||
fmt.Printf(" %s: FAILED — %s\n", name, r.GetError())
|
||||
}
|
||||
}
|
||||
|
||||
// parseServiceArg splits a "service/component" argument into its parts.
|
||||
func parseServiceArg(arg string) (service, component string) {
|
||||
parts := strings.SplitN(arg, "/", 2)
|
||||
@@ -125,8 +172,9 @@ func loadServiceDef(cmd *cobra.Command, cfg *config.CLIConfig, serviceName strin
|
||||
// ServiceInfo, not ServiceSpec.
|
||||
func serviceSpecFromInfo(info *mcpv1.ServiceInfo) *mcpv1.ServiceSpec {
|
||||
spec := &mcpv1.ServiceSpec{
|
||||
Name: info.GetName(),
|
||||
Active: info.GetActive(),
|
||||
Name: info.GetName(),
|
||||
Active: info.GetActive(),
|
||||
Comment: info.GetComment(),
|
||||
}
|
||||
for _, c := range info.GetComponents() {
|
||||
spec.Components = append(spec.Components, &mcpv1.ComponentSpec{
|
||||
|
||||
@@ -52,6 +52,43 @@ func dialAgent(address string, cfg *config.CLIConfig) (mcpv1.McpAgentServiceClie
|
||||
return mcpv1.NewMcpAgentServiceClient(conn), conn, nil
|
||||
}
|
||||
|
||||
// dialMaster connects to the master at the given address and returns a gRPC
|
||||
// client for the McpMasterService.
|
||||
func dialMaster(address string, cfg *config.CLIConfig) (mcpv1.McpMasterServiceClient, *grpc.ClientConn, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
if cfg.MCIAS.CACert != "" {
|
||||
caCert, err := os.ReadFile(cfg.MCIAS.CACert) //nolint:gosec // trusted config path
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read CA cert %q: %w", cfg.MCIAS.CACert, err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caCert) {
|
||||
return nil, nil, fmt.Errorf("invalid CA cert %q", cfg.MCIAS.CACert)
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
token, err := loadBearerToken(cfg)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("load token: %w", err)
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(
|
||||
address,
|
||||
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
||||
grpc.WithUnaryInterceptor(tokenInterceptor(token)),
|
||||
grpc.WithStreamInterceptor(streamTokenInterceptor(token)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("dial master %q: %w", address, err)
|
||||
}
|
||||
|
||||
return mcpv1.NewMcpMasterServiceClient(conn), conn, nil
|
||||
}
|
||||
|
||||
// tokenInterceptor returns a gRPC client interceptor that attaches the
|
||||
// bearer token to outgoing RPC metadata.
|
||||
func tokenInterceptor(token string) grpc.UnaryClientInterceptor {
|
||||
|
||||
180
cmd/mcp/edge.go
Normal file
180
cmd/mcp/edge.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||
)
|
||||
|
||||
func edgeCmd() *cobra.Command {
|
||||
var nodeName string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "edge",
|
||||
Short: "Manage edge routes (scaffolding — will be replaced by master)",
|
||||
}
|
||||
|
||||
list := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List edge routes on a node",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if nodeName == "" {
|
||||
return fmt.Errorf("--node is required")
|
||||
}
|
||||
return runEdgeList(nodeName)
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
backendHostname string
|
||||
backendPort int
|
||||
)
|
||||
|
||||
setup := &cobra.Command{
|
||||
Use: "setup <hostname>",
|
||||
Short: "Set up an edge route (provisions cert, registers mc-proxy route)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if nodeName == "" {
|
||||
return fmt.Errorf("--node is required")
|
||||
}
|
||||
if backendHostname == "" {
|
||||
return fmt.Errorf("--backend-hostname is required")
|
||||
}
|
||||
if backendPort == 0 {
|
||||
return fmt.Errorf("--backend-port is required")
|
||||
}
|
||||
return runEdgeSetup(nodeName, args[0], backendHostname, backendPort)
|
||||
},
|
||||
}
|
||||
setup.Flags().StringVar(&backendHostname, "backend-hostname", "", "internal .svc.mcp hostname")
|
||||
setup.Flags().IntVar(&backendPort, "backend-port", 0, "port on worker's mc-proxy")
|
||||
|
||||
remove := &cobra.Command{
|
||||
Use: "remove <hostname>",
|
||||
Short: "Remove an edge route",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if nodeName == "" {
|
||||
return fmt.Errorf("--node is required")
|
||||
}
|
||||
return runEdgeRemove(nodeName, args[0])
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&nodeName, "node", "n", "", "target node (required)")
|
||||
cmd.AddCommand(list, setup, remove)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runEdgeList(nodeName string) error {
|
||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
address, err := findNodeAddress(cfg, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, conn, err := dialAgent(address, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial agent: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.ListEdgeRoutes(ctx, &mcpv1.ListEdgeRoutesRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("list edge routes: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.GetRoutes()) == 0 {
|
||||
fmt.Printf("No edge routes on %s\n", nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Edge routes on %s:\n", nodeName)
|
||||
for _, r := range resp.GetRoutes() {
|
||||
expires := r.GetCertExpires()
|
||||
if expires == "" {
|
||||
expires = "unknown"
|
||||
}
|
||||
fmt.Printf(" %s → %s:%d cert_expires=%s\n",
|
||||
r.GetHostname(), r.GetBackendHostname(), r.GetBackendPort(), expires)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runEdgeSetup(nodeName, hostname, backendHostname string, backendPort int) error {
|
||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
address, err := findNodeAddress(cfg, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, conn, err := dialAgent(address, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial agent: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = client.SetupEdgeRoute(ctx, &mcpv1.SetupEdgeRouteRequest{
|
||||
Hostname: hostname,
|
||||
BackendHostname: backendHostname,
|
||||
BackendPort: int32(backendPort), //nolint:gosec // port is a small positive integer
|
||||
BackendTls: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setup edge route: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("edge route established: %s → %s:%d on %s\n", hostname, backendHostname, backendPort, nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runEdgeRemove(nodeName, hostname string) error {
|
||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
address, err := findNodeAddress(cfg, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, conn, err := dialAgent(address, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial agent: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = client.RemoveEdgeRoute(ctx, &mcpv1.RemoveEdgeRouteRequest{
|
||||
Hostname: hostname,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove edge route: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("edge route removed: %s on %s\n", hostname, nodeName)
|
||||
return nil
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
func stopCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stop <service>",
|
||||
Short: "Stop all components, set active=false",
|
||||
Use: "stop <service>[/<component>]",
|
||||
Short: "Stop components (or all), set active=false",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||
@@ -23,7 +23,7 @@ func stopCmd() *cobra.Command {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
serviceName := args[0]
|
||||
serviceName, component := parseServiceArg(args[0])
|
||||
defPath := filepath.Join(cfg.Services.Dir, serviceName+".toml")
|
||||
|
||||
def, err := servicedef.Load(defPath)
|
||||
@@ -31,10 +31,13 @@ func stopCmd() *cobra.Command {
|
||||
return fmt.Errorf("load service def: %w", err)
|
||||
}
|
||||
|
||||
active := false
|
||||
def.Active = &active
|
||||
if err := servicedef.Write(defPath, def); err != nil {
|
||||
return fmt.Errorf("write service def: %w", err)
|
||||
// Only flip active=false when stopping the whole service.
|
||||
if component == "" {
|
||||
active := false
|
||||
def.Active = &active
|
||||
if err := servicedef.Write(defPath, def); err != nil {
|
||||
return fmt.Errorf("write service def: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
address, err := findNodeAddress(cfg, def.Node)
|
||||
@@ -49,7 +52,8 @@ func stopCmd() *cobra.Command {
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
resp, err := client.StopService(context.Background(), &mcpv1.StopServiceRequest{
|
||||
Name: serviceName,
|
||||
Name: serviceName,
|
||||
Component: component,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("stop service: %w", err)
|
||||
@@ -63,8 +67,8 @@ func stopCmd() *cobra.Command {
|
||||
|
||||
func startCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "start <service>",
|
||||
Short: "Start all components, set active=true",
|
||||
Use: "start <service>[/<component>]",
|
||||
Short: "Start components (or all), set active=true",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||
@@ -72,7 +76,7 @@ func startCmd() *cobra.Command {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
serviceName := args[0]
|
||||
serviceName, component := parseServiceArg(args[0])
|
||||
defPath := filepath.Join(cfg.Services.Dir, serviceName+".toml")
|
||||
|
||||
def, err := servicedef.Load(defPath)
|
||||
@@ -80,10 +84,13 @@ func startCmd() *cobra.Command {
|
||||
return fmt.Errorf("load service def: %w", err)
|
||||
}
|
||||
|
||||
active := true
|
||||
def.Active = &active
|
||||
if err := servicedef.Write(defPath, def); err != nil {
|
||||
return fmt.Errorf("write service def: %w", err)
|
||||
// Only flip active=true when starting the whole service.
|
||||
if component == "" {
|
||||
active := true
|
||||
def.Active = &active
|
||||
if err := servicedef.Write(defPath, def); err != nil {
|
||||
return fmt.Errorf("write service def: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
address, err := findNodeAddress(cfg, def.Node)
|
||||
@@ -98,7 +105,8 @@ func startCmd() *cobra.Command {
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
resp, err := client.StartService(context.Background(), &mcpv1.StartServiceRequest{
|
||||
Name: serviceName,
|
||||
Name: serviceName,
|
||||
Component: component,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("start service: %w", err)
|
||||
@@ -112,8 +120,8 @@ func startCmd() *cobra.Command {
|
||||
|
||||
func restartCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "restart <service>",
|
||||
Short: "Restart all components",
|
||||
Use: "restart <service>[/<component>]",
|
||||
Short: "Restart components (or all)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||
@@ -121,7 +129,7 @@ func restartCmd() *cobra.Command {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
serviceName := args[0]
|
||||
serviceName, component := parseServiceArg(args[0])
|
||||
defPath := filepath.Join(cfg.Services.Dir, serviceName+".toml")
|
||||
|
||||
def, err := servicedef.Load(defPath)
|
||||
@@ -141,7 +149,8 @@ func restartCmd() *cobra.Command {
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
resp, err := client.RestartService(context.Background(), &mcpv1.RestartServiceRequest{
|
||||
Name: serviceName,
|
||||
Name: serviceName,
|
||||
Component: component,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("restart service: %w", err)
|
||||
|
||||
@@ -54,6 +54,7 @@ func main() {
|
||||
root.AddCommand(editCmd())
|
||||
root.AddCommand(dnsCmd())
|
||||
root.AddCommand(routeCmd())
|
||||
root.AddCommand(edgeCmd())
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -28,17 +28,26 @@ func routeCmd() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
routeMode string
|
||||
backendTLS bool
|
||||
tlsCert string
|
||||
tlsKey string
|
||||
)
|
||||
|
||||
add := &cobra.Command{
|
||||
Use: "add <listener> <hostname> <backend>",
|
||||
Short: "Add a route to mc-proxy",
|
||||
Long: "Add a route. Example: mcp route add -n rift :443 mcq.metacircular.net 100.95.252.120:443",
|
||||
Long: "Add a route. Example: mcp route add -n rift :443 mcq.svc.mcp.metacircular.net 127.0.0.1:48080 --mode l7 --tls-cert /srv/mc-proxy/certs/mcq.pem --tls-key /srv/mc-proxy/certs/mcq.key",
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return runRouteAdd(nodeName, args)
|
||||
return runRouteAdd(nodeName, args, routeMode, backendTLS, tlsCert, tlsKey)
|
||||
},
|
||||
}
|
||||
add.Flags().String("mode", "l4", "route mode (l4 or l7)")
|
||||
add.Flags().Bool("backend-tls", false, "re-encrypt traffic to backend")
|
||||
add.Flags().StringVar(&routeMode, "mode", "l4", "route mode (l4 or l7)")
|
||||
add.Flags().BoolVar(&backendTLS, "backend-tls", false, "re-encrypt traffic to backend")
|
||||
add.Flags().StringVar(&tlsCert, "tls-cert", "", "path to TLS cert on the node (required for l7)")
|
||||
add.Flags().StringVar(&tlsKey, "tls-key", "", "path to TLS key on the node (required for l7)")
|
||||
|
||||
remove := &cobra.Command{
|
||||
Use: "remove <listener> <hostname>",
|
||||
@@ -138,7 +147,7 @@ func printRoutes(nodeName string, resp *mcpv1.ListProxyRoutesResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
func runRouteAdd(nodeName string, args []string) error {
|
||||
func runRouteAdd(nodeName string, args []string, mode string, backendTLS bool, tlsCert, tlsKey string) error {
|
||||
if nodeName == "" {
|
||||
return fmt.Errorf("--node is required")
|
||||
}
|
||||
@@ -166,12 +175,16 @@ func runRouteAdd(nodeName string, args []string) error {
|
||||
ListenerAddr: args[0],
|
||||
Hostname: args[1],
|
||||
Backend: args[2],
|
||||
Mode: mode,
|
||||
BackendTls: backendTLS,
|
||||
TlsCert: tlsCert,
|
||||
TlsKey: tlsKey,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("add route: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Added route: %s → %s on %s (%s)\n", args[1], args[2], args[0], nodeName)
|
||||
fmt.Printf("Added route: %s %s → %s on %s (%s)\n", mode, args[1], args[2], args[0], nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ import (
|
||||
)
|
||||
|
||||
func undeployCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
var direct bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "undeploy <service>",
|
||||
Short: "Fully undeploy a service: remove routes, DNS, certs, and containers",
|
||||
Args: cobra.ExactArgs(1),
|
||||
@@ -38,6 +40,11 @@ func undeployCmd() *cobra.Command {
|
||||
return fmt.Errorf("write service def: %w", err)
|
||||
}
|
||||
|
||||
// Route through master if configured and not in direct mode.
|
||||
if cfg.Master != nil && cfg.Master.Address != "" && !direct {
|
||||
return undeployViaMaster(cfg, serviceName)
|
||||
}
|
||||
|
||||
address, err := findNodeAddress(cfg, def.Node)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -60,4 +67,28 @@ func undeployCmd() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&direct, "direct", false, "bypass master, undeploy directly via agent")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func undeployViaMaster(cfg *config.CLIConfig, serviceName string) error {
|
||||
client, conn, err := dialMaster(cfg.Master.Address, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial master: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
resp, err := client.Undeploy(context.Background(), &mcpv1.MasterUndeployRequest{
|
||||
ServiceName: serviceName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("master undeploy: %w", err)
|
||||
}
|
||||
|
||||
if resp.GetSuccess() {
|
||||
fmt.Printf(" %s: undeployed\n", serviceName)
|
||||
} else {
|
||||
return fmt.Errorf("undeploy failed: %s", resp.GetError())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
94
deploy/examples/mcp-master.toml
Normal file
94
deploy/examples/mcp-master.toml
Normal file
@@ -0,0 +1,94 @@
|
||||
# MCP Master configuration
|
||||
#
|
||||
# Default location: /srv/mcp-master/mcp-master.toml
|
||||
# Override with: mcp-master server --config /path/to/mcp-master.toml
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# gRPC server
|
||||
# ------------------------------------------------------------------
|
||||
[server]
|
||||
# Listen address for the gRPC server. Bind to the Tailnet interface.
|
||||
grpc_addr = "100.95.252.120:9555"
|
||||
tls_cert = "/srv/mcp-master/certs/cert.pem"
|
||||
tls_key = "/srv/mcp-master/certs/key.pem"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Database
|
||||
# ------------------------------------------------------------------
|
||||
[database]
|
||||
path = "/srv/mcp-master/master.db"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# MCIAS (for validating inbound CLI/agent tokens)
|
||||
# ------------------------------------------------------------------
|
||||
[mcias]
|
||||
server_url = "https://mcias.metacircular.net:8443"
|
||||
ca_cert = "/srv/mcp-master/certs/ca.pem"
|
||||
service_name = "mcp-master"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Master identity (for dialing agents)
|
||||
# ------------------------------------------------------------------
|
||||
[master]
|
||||
# Path to the MCIAS service token file used by the master to
|
||||
# authenticate to agents when forwarding deploys and edge routes.
|
||||
service_token_path = "/srv/mcp-master/mcias-token"
|
||||
|
||||
# CA cert for verifying agent TLS certificates.
|
||||
ca_cert = "/srv/mcp-master/certs/ca.pem"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Edge routing
|
||||
# ------------------------------------------------------------------
|
||||
[edge]
|
||||
# Public hostnames in service definitions must fall under one of these
|
||||
# domains. Validation uses proper domain label matching.
|
||||
allowed_domains = ["metacircular.net", "wntrmute.net"]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Agent registration
|
||||
# ------------------------------------------------------------------
|
||||
[registration]
|
||||
# MCIAS service identities permitted to register.
|
||||
allowed_agents = ["agent-rift", "agent-svc", "agent-orion"]
|
||||
|
||||
# Maximum registered nodes.
|
||||
max_nodes = 16
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Timeouts
|
||||
# ------------------------------------------------------------------
|
||||
[timeouts]
|
||||
deploy = "5m"
|
||||
edge_route = "30s"
|
||||
health_check = "5s"
|
||||
undeploy = "2m"
|
||||
snapshot = "10m"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DNS (MCNS)
|
||||
# ------------------------------------------------------------------
|
||||
[mcns]
|
||||
server_url = "https://mcns.svc.mcp.metacircular.net:8443"
|
||||
ca_cert = "/srv/mcp-master/certs/ca.pem"
|
||||
token_path = "/srv/mcp-master/mcns-token"
|
||||
zone = "svc.mcp.metacircular.net"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Logging
|
||||
# ------------------------------------------------------------------
|
||||
[log]
|
||||
level = "info"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Bootstrap nodes
|
||||
# ------------------------------------------------------------------
|
||||
[[nodes]]
|
||||
name = "rift"
|
||||
address = "100.95.252.120:9444"
|
||||
role = "master"
|
||||
|
||||
[[nodes]]
|
||||
name = "svc"
|
||||
address = "100.106.232.4:9555"
|
||||
role = "edge"
|
||||
@@ -10,7 +10,7 @@
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
version = pkgs.lib.removePrefix "v" (self.gitDescribe or self.shortRev or self.dirtyShortRev or "unknown");
|
||||
version = "0.8.3";
|
||||
in
|
||||
{
|
||||
packages.${system} = {
|
||||
|
||||
849
gen/mcp/v1/master.pb.go
Normal file
849
gen/mcp/v1/master.pb.go
Normal file
@@ -0,0 +1,849 @@
|
||||
// McpMasterService: Multi-node orchestration for the Metacircular platform.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.32.1
|
||||
// source: proto/mcp/v1/master.proto
|
||||
|
||||
package mcpv1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type MasterDeployRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Service *ServiceSpec `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MasterDeployRequest) Reset() {
|
||||
*x = MasterDeployRequest{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MasterDeployRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MasterDeployRequest) ProtoMessage() {}
|
||||
|
||||
func (x *MasterDeployRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MasterDeployRequest.ProtoReflect.Descriptor instead.
|
||||
func (*MasterDeployRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *MasterDeployRequest) GetService() *ServiceSpec {
|
||||
if x != nil {
|
||||
return x.Service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MasterDeployResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Node string `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"` // node the service was placed on
|
||||
Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` // true only if ALL steps succeeded
|
||||
Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"`
|
||||
// Per-step results for operator visibility.
|
||||
DeployResult *StepResult `protobuf:"bytes,4,opt,name=deploy_result,json=deployResult,proto3" json:"deploy_result,omitempty"`
|
||||
EdgeRouteResult *StepResult `protobuf:"bytes,5,opt,name=edge_route_result,json=edgeRouteResult,proto3" json:"edge_route_result,omitempty"`
|
||||
DnsResult *StepResult `protobuf:"bytes,6,opt,name=dns_result,json=dnsResult,proto3" json:"dns_result,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MasterDeployResponse) Reset() {
|
||||
*x = MasterDeployResponse{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MasterDeployResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MasterDeployResponse) ProtoMessage() {}
|
||||
|
||||
func (x *MasterDeployResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MasterDeployResponse.ProtoReflect.Descriptor instead.
|
||||
func (*MasterDeployResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *MasterDeployResponse) GetNode() string {
|
||||
if x != nil {
|
||||
return x.Node
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MasterDeployResponse) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *MasterDeployResponse) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MasterDeployResponse) GetDeployResult() *StepResult {
|
||||
if x != nil {
|
||||
return x.DeployResult
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MasterDeployResponse) GetEdgeRouteResult() *StepResult {
|
||||
if x != nil {
|
||||
return x.EdgeRouteResult
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MasterDeployResponse) GetDnsResult() *StepResult {
|
||||
if x != nil {
|
||||
return x.DnsResult
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StepResult struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Step string `protobuf:"bytes,1,opt,name=step,proto3" json:"step,omitempty"`
|
||||
Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"`
|
||||
Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StepResult) Reset() {
|
||||
*x = StepResult{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StepResult) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StepResult) ProtoMessage() {}
|
||||
|
||||
func (x *StepResult) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StepResult.ProtoReflect.Descriptor instead.
|
||||
func (*StepResult) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *StepResult) GetStep() string {
|
||||
if x != nil {
|
||||
return x.Step
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StepResult) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *StepResult) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type MasterUndeployRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MasterUndeployRequest) Reset() {
|
||||
*x = MasterUndeployRequest{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MasterUndeployRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MasterUndeployRequest) ProtoMessage() {}
|
||||
|
||||
func (x *MasterUndeployRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MasterUndeployRequest.ProtoReflect.Descriptor instead.
|
||||
func (*MasterUndeployRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *MasterUndeployRequest) GetServiceName() string {
|
||||
if x != nil {
|
||||
return x.ServiceName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type MasterUndeployResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
|
||||
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MasterUndeployResponse) Reset() {
|
||||
*x = MasterUndeployResponse{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MasterUndeployResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MasterUndeployResponse) ProtoMessage() {}
|
||||
|
||||
func (x *MasterUndeployResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MasterUndeployResponse.ProtoReflect.Descriptor instead.
|
||||
func (*MasterUndeployResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *MasterUndeployResponse) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *MasterUndeployResponse) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type MasterStatusRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // empty = all services
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MasterStatusRequest) Reset() {
|
||||
*x = MasterStatusRequest{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MasterStatusRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MasterStatusRequest) ProtoMessage() {}
|
||||
|
||||
func (x *MasterStatusRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MasterStatusRequest.ProtoReflect.Descriptor instead.
|
||||
func (*MasterStatusRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *MasterStatusRequest) GetServiceName() string {
|
||||
if x != nil {
|
||||
return x.ServiceName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type MasterStatusResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Services []*ServiceStatus `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MasterStatusResponse) Reset() {
|
||||
*x = MasterStatusResponse{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MasterStatusResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MasterStatusResponse) ProtoMessage() {}
|
||||
|
||||
func (x *MasterStatusResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MasterStatusResponse.ProtoReflect.Descriptor instead.
|
||||
func (*MasterStatusResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *MasterStatusResponse) GetServices() []*ServiceStatus {
|
||||
if x != nil {
|
||||
return x.Services
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ServiceStatus struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Node string `protobuf:"bytes,2,opt,name=node,proto3" json:"node,omitempty"`
|
||||
Tier string `protobuf:"bytes,3,opt,name=tier,proto3" json:"tier,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // "running", "stopped", "unhealthy", "unknown"
|
||||
EdgeRoutes []*EdgeRouteStatus `protobuf:"bytes,5,rep,name=edge_routes,json=edgeRoutes,proto3" json:"edge_routes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ServiceStatus) Reset() {
|
||||
*x = ServiceStatus{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ServiceStatus) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ServiceStatus) ProtoMessage() {}
|
||||
|
||||
func (x *ServiceStatus) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ServiceStatus.ProtoReflect.Descriptor instead.
|
||||
func (*ServiceStatus) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *ServiceStatus) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ServiceStatus) GetNode() string {
|
||||
if x != nil {
|
||||
return x.Node
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ServiceStatus) GetTier() string {
|
||||
if x != nil {
|
||||
return x.Tier
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ServiceStatus) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ServiceStatus) GetEdgeRoutes() []*EdgeRouteStatus {
|
||||
if x != nil {
|
||||
return x.EdgeRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type EdgeRouteStatus struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"`
|
||||
EdgeNode string `protobuf:"bytes,2,opt,name=edge_node,json=edgeNode,proto3" json:"edge_node,omitempty"`
|
||||
CertExpires string `protobuf:"bytes,3,opt,name=cert_expires,json=certExpires,proto3" json:"cert_expires,omitempty"` // RFC3339
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EdgeRouteStatus) Reset() {
|
||||
*x = EdgeRouteStatus{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EdgeRouteStatus) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EdgeRouteStatus) ProtoMessage() {}
|
||||
|
||||
func (x *EdgeRouteStatus) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EdgeRouteStatus.ProtoReflect.Descriptor instead.
|
||||
func (*EdgeRouteStatus) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *EdgeRouteStatus) GetHostname() string {
|
||||
if x != nil {
|
||||
return x.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EdgeRouteStatus) GetEdgeNode() string {
|
||||
if x != nil {
|
||||
return x.EdgeNode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EdgeRouteStatus) GetCertExpires() string {
|
||||
if x != nil {
|
||||
return x.CertExpires
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ListNodesRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListNodesRequest) Reset() {
|
||||
*x = ListNodesRequest{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListNodesRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListNodesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListNodesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListNodesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListNodesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
type ListNodesResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Nodes []*NodeInfo `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListNodesResponse) Reset() {
|
||||
*x = ListNodesResponse{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListNodesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListNodesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListNodesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListNodesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListNodesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *ListNodesResponse) GetNodes() []*NodeInfo {
|
||||
if x != nil {
|
||||
return x.Nodes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NodeInfo struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"`
|
||||
Address string `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"`
|
||||
Arch string `protobuf:"bytes,4,opt,name=arch,proto3" json:"arch,omitempty"`
|
||||
Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // "healthy", "unhealthy", "unknown"
|
||||
Containers int32 `protobuf:"varint,6,opt,name=containers,proto3" json:"containers,omitempty"`
|
||||
LastHeartbeat string `protobuf:"bytes,7,opt,name=last_heartbeat,json=lastHeartbeat,proto3" json:"last_heartbeat,omitempty"` // RFC3339
|
||||
Services int32 `protobuf:"varint,8,opt,name=services,proto3" json:"services,omitempty"` // placement count
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *NodeInfo) Reset() {
|
||||
*x = NodeInfo{}
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *NodeInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*NodeInfo) ProtoMessage() {}
|
||||
|
||||
func (x *NodeInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_mcp_v1_master_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use NodeInfo.ProtoReflect.Descriptor instead.
|
||||
func (*NodeInfo) Descriptor() ([]byte, []int) {
|
||||
return file_proto_mcp_v1_master_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *NodeInfo) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NodeInfo) GetRole() string {
|
||||
if x != nil {
|
||||
return x.Role
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NodeInfo) GetAddress() string {
|
||||
if x != nil {
|
||||
return x.Address
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NodeInfo) GetArch() string {
|
||||
if x != nil {
|
||||
return x.Arch
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NodeInfo) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NodeInfo) GetContainers() int32 {
|
||||
if x != nil {
|
||||
return x.Containers
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *NodeInfo) GetLastHeartbeat() string {
|
||||
if x != nil {
|
||||
return x.LastHeartbeat
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NodeInfo) GetServices() int32 {
|
||||
if x != nil {
|
||||
return x.Services
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_proto_mcp_v1_master_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proto_mcp_v1_master_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19proto/mcp/v1/master.proto\x12\x06mcp.v1\x1a\x16proto/mcp/v1/mcp.proto\"D\n" +
|
||||
"\x13MasterDeployRequest\x12-\n" +
|
||||
"\aservice\x18\x01 \x01(\v2\x13.mcp.v1.ServiceSpecR\aservice\"\x86\x02\n" +
|
||||
"\x14MasterDeployResponse\x12\x12\n" +
|
||||
"\x04node\x18\x01 \x01(\tR\x04node\x12\x18\n" +
|
||||
"\asuccess\x18\x02 \x01(\bR\asuccess\x12\x14\n" +
|
||||
"\x05error\x18\x03 \x01(\tR\x05error\x127\n" +
|
||||
"\rdeploy_result\x18\x04 \x01(\v2\x12.mcp.v1.StepResultR\fdeployResult\x12>\n" +
|
||||
"\x11edge_route_result\x18\x05 \x01(\v2\x12.mcp.v1.StepResultR\x0fedgeRouteResult\x121\n" +
|
||||
"\n" +
|
||||
"dns_result\x18\x06 \x01(\v2\x12.mcp.v1.StepResultR\tdnsResult\"P\n" +
|
||||
"\n" +
|
||||
"StepResult\x12\x12\n" +
|
||||
"\x04step\x18\x01 \x01(\tR\x04step\x12\x18\n" +
|
||||
"\asuccess\x18\x02 \x01(\bR\asuccess\x12\x14\n" +
|
||||
"\x05error\x18\x03 \x01(\tR\x05error\":\n" +
|
||||
"\x15MasterUndeployRequest\x12!\n" +
|
||||
"\fservice_name\x18\x01 \x01(\tR\vserviceName\"H\n" +
|
||||
"\x16MasterUndeployResponse\x12\x18\n" +
|
||||
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" +
|
||||
"\x05error\x18\x02 \x01(\tR\x05error\"8\n" +
|
||||
"\x13MasterStatusRequest\x12!\n" +
|
||||
"\fservice_name\x18\x01 \x01(\tR\vserviceName\"I\n" +
|
||||
"\x14MasterStatusResponse\x121\n" +
|
||||
"\bservices\x18\x01 \x03(\v2\x15.mcp.v1.ServiceStatusR\bservices\"\x9d\x01\n" +
|
||||
"\rServiceStatus\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
||||
"\x04node\x18\x02 \x01(\tR\x04node\x12\x12\n" +
|
||||
"\x04tier\x18\x03 \x01(\tR\x04tier\x12\x16\n" +
|
||||
"\x06status\x18\x04 \x01(\tR\x06status\x128\n" +
|
||||
"\vedge_routes\x18\x05 \x03(\v2\x17.mcp.v1.EdgeRouteStatusR\n" +
|
||||
"edgeRoutes\"m\n" +
|
||||
"\x0fEdgeRouteStatus\x12\x1a\n" +
|
||||
"\bhostname\x18\x01 \x01(\tR\bhostname\x12\x1b\n" +
|
||||
"\tedge_node\x18\x02 \x01(\tR\bedgeNode\x12!\n" +
|
||||
"\fcert_expires\x18\x03 \x01(\tR\vcertExpires\"\x12\n" +
|
||||
"\x10ListNodesRequest\";\n" +
|
||||
"\x11ListNodesResponse\x12&\n" +
|
||||
"\x05nodes\x18\x01 \x03(\v2\x10.mcp.v1.NodeInfoR\x05nodes\"\xdb\x01\n" +
|
||||
"\bNodeInfo\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
||||
"\x04role\x18\x02 \x01(\tR\x04role\x12\x18\n" +
|
||||
"\aaddress\x18\x03 \x01(\tR\aaddress\x12\x12\n" +
|
||||
"\x04arch\x18\x04 \x01(\tR\x04arch\x12\x16\n" +
|
||||
"\x06status\x18\x05 \x01(\tR\x06status\x12\x1e\n" +
|
||||
"\n" +
|
||||
"containers\x18\x06 \x01(\x05R\n" +
|
||||
"containers\x12%\n" +
|
||||
"\x0elast_heartbeat\x18\a \x01(\tR\rlastHeartbeat\x12\x1a\n" +
|
||||
"\bservices\x18\b \x01(\x05R\bservices2\xa9\x02\n" +
|
||||
"\x10McpMasterService\x12C\n" +
|
||||
"\x06Deploy\x12\x1b.mcp.v1.MasterDeployRequest\x1a\x1c.mcp.v1.MasterDeployResponse\x12I\n" +
|
||||
"\bUndeploy\x12\x1d.mcp.v1.MasterUndeployRequest\x1a\x1e.mcp.v1.MasterUndeployResponse\x12C\n" +
|
||||
"\x06Status\x12\x1b.mcp.v1.MasterStatusRequest\x1a\x1c.mcp.v1.MasterStatusResponse\x12@\n" +
|
||||
"\tListNodes\x12\x18.mcp.v1.ListNodesRequest\x1a\x19.mcp.v1.ListNodesResponseB*Z(git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_proto_mcp_v1_master_proto_rawDescOnce sync.Once
|
||||
file_proto_mcp_v1_master_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_proto_mcp_v1_master_proto_rawDescGZIP() []byte {
|
||||
file_proto_mcp_v1_master_proto_rawDescOnce.Do(func() {
|
||||
file_proto_mcp_v1_master_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcp_v1_master_proto_rawDesc), len(file_proto_mcp_v1_master_proto_rawDesc)))
|
||||
})
|
||||
return file_proto_mcp_v1_master_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proto_mcp_v1_master_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_proto_mcp_v1_master_proto_goTypes = []any{
|
||||
(*MasterDeployRequest)(nil), // 0: mcp.v1.MasterDeployRequest
|
||||
(*MasterDeployResponse)(nil), // 1: mcp.v1.MasterDeployResponse
|
||||
(*StepResult)(nil), // 2: mcp.v1.StepResult
|
||||
(*MasterUndeployRequest)(nil), // 3: mcp.v1.MasterUndeployRequest
|
||||
(*MasterUndeployResponse)(nil), // 4: mcp.v1.MasterUndeployResponse
|
||||
(*MasterStatusRequest)(nil), // 5: mcp.v1.MasterStatusRequest
|
||||
(*MasterStatusResponse)(nil), // 6: mcp.v1.MasterStatusResponse
|
||||
(*ServiceStatus)(nil), // 7: mcp.v1.ServiceStatus
|
||||
(*EdgeRouteStatus)(nil), // 8: mcp.v1.EdgeRouteStatus
|
||||
(*ListNodesRequest)(nil), // 9: mcp.v1.ListNodesRequest
|
||||
(*ListNodesResponse)(nil), // 10: mcp.v1.ListNodesResponse
|
||||
(*NodeInfo)(nil), // 11: mcp.v1.NodeInfo
|
||||
(*ServiceSpec)(nil), // 12: mcp.v1.ServiceSpec
|
||||
}
|
||||
var file_proto_mcp_v1_master_proto_depIdxs = []int32{
|
||||
12, // 0: mcp.v1.MasterDeployRequest.service:type_name -> mcp.v1.ServiceSpec
|
||||
2, // 1: mcp.v1.MasterDeployResponse.deploy_result:type_name -> mcp.v1.StepResult
|
||||
2, // 2: mcp.v1.MasterDeployResponse.edge_route_result:type_name -> mcp.v1.StepResult
|
||||
2, // 3: mcp.v1.MasterDeployResponse.dns_result:type_name -> mcp.v1.StepResult
|
||||
7, // 4: mcp.v1.MasterStatusResponse.services:type_name -> mcp.v1.ServiceStatus
|
||||
8, // 5: mcp.v1.ServiceStatus.edge_routes:type_name -> mcp.v1.EdgeRouteStatus
|
||||
11, // 6: mcp.v1.ListNodesResponse.nodes:type_name -> mcp.v1.NodeInfo
|
||||
0, // 7: mcp.v1.McpMasterService.Deploy:input_type -> mcp.v1.MasterDeployRequest
|
||||
3, // 8: mcp.v1.McpMasterService.Undeploy:input_type -> mcp.v1.MasterUndeployRequest
|
||||
5, // 9: mcp.v1.McpMasterService.Status:input_type -> mcp.v1.MasterStatusRequest
|
||||
9, // 10: mcp.v1.McpMasterService.ListNodes:input_type -> mcp.v1.ListNodesRequest
|
||||
1, // 11: mcp.v1.McpMasterService.Deploy:output_type -> mcp.v1.MasterDeployResponse
|
||||
4, // 12: mcp.v1.McpMasterService.Undeploy:output_type -> mcp.v1.MasterUndeployResponse
|
||||
6, // 13: mcp.v1.McpMasterService.Status:output_type -> mcp.v1.MasterStatusResponse
|
||||
10, // 14: mcp.v1.McpMasterService.ListNodes:output_type -> mcp.v1.ListNodesResponse
|
||||
11, // [11:15] is the sub-list for method output_type
|
||||
7, // [7:11] is the sub-list for method input_type
|
||||
7, // [7:7] is the sub-list for extension type_name
|
||||
7, // [7:7] is the sub-list for extension extendee
|
||||
0, // [0:7] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proto_mcp_v1_master_proto_init() }
|
||||
func file_proto_mcp_v1_master_proto_init() {
|
||||
if File_proto_mcp_v1_master_proto != nil {
|
||||
return
|
||||
}
|
||||
file_proto_mcp_v1_mcp_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcp_v1_master_proto_rawDesc), len(file_proto_mcp_v1_master_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_proto_mcp_v1_master_proto_goTypes,
|
||||
DependencyIndexes: file_proto_mcp_v1_master_proto_depIdxs,
|
||||
MessageInfos: file_proto_mcp_v1_master_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proto_mcp_v1_master_proto = out.File
|
||||
file_proto_mcp_v1_master_proto_goTypes = nil
|
||||
file_proto_mcp_v1_master_proto_depIdxs = nil
|
||||
}
|
||||
247
gen/mcp/v1/master_grpc.pb.go
Normal file
247
gen/mcp/v1/master_grpc.pb.go
Normal file
@@ -0,0 +1,247 @@
|
||||
// McpMasterService: Multi-node orchestration for the Metacircular platform.
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.1
|
||||
// - protoc v6.32.1
|
||||
// source: proto/mcp/v1/master.proto
|
||||
|
||||
package mcpv1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
McpMasterService_Deploy_FullMethodName = "/mcp.v1.McpMasterService/Deploy"
|
||||
McpMasterService_Undeploy_FullMethodName = "/mcp.v1.McpMasterService/Undeploy"
|
||||
McpMasterService_Status_FullMethodName = "/mcp.v1.McpMasterService/Status"
|
||||
McpMasterService_ListNodes_FullMethodName = "/mcp.v1.McpMasterService/ListNodes"
|
||||
)
|
||||
|
||||
// McpMasterServiceClient is the client API for McpMasterService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// McpMasterService coordinates multi-node deployments. The CLI sends
|
||||
// deploy/undeploy/status requests to the master, which places services on
|
||||
// nodes, forwards to agents, and coordinates edge routing.
|
||||
type McpMasterServiceClient interface {
|
||||
// CLI operations.
|
||||
Deploy(ctx context.Context, in *MasterDeployRequest, opts ...grpc.CallOption) (*MasterDeployResponse, error)
|
||||
Undeploy(ctx context.Context, in *MasterUndeployRequest, opts ...grpc.CallOption) (*MasterUndeployResponse, error)
|
||||
Status(ctx context.Context, in *MasterStatusRequest, opts ...grpc.CallOption) (*MasterStatusResponse, error)
|
||||
ListNodes(ctx context.Context, in *ListNodesRequest, opts ...grpc.CallOption) (*ListNodesResponse, error)
|
||||
}
|
||||
|
||||
type mcpMasterServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewMcpMasterServiceClient(cc grpc.ClientConnInterface) McpMasterServiceClient {
|
||||
return &mcpMasterServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *mcpMasterServiceClient) Deploy(ctx context.Context, in *MasterDeployRequest, opts ...grpc.CallOption) (*MasterDeployResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(MasterDeployResponse)
|
||||
err := c.cc.Invoke(ctx, McpMasterService_Deploy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mcpMasterServiceClient) Undeploy(ctx context.Context, in *MasterUndeployRequest, opts ...grpc.CallOption) (*MasterUndeployResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(MasterUndeployResponse)
|
||||
err := c.cc.Invoke(ctx, McpMasterService_Undeploy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mcpMasterServiceClient) Status(ctx context.Context, in *MasterStatusRequest, opts ...grpc.CallOption) (*MasterStatusResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(MasterStatusResponse)
|
||||
err := c.cc.Invoke(ctx, McpMasterService_Status_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mcpMasterServiceClient) ListNodes(ctx context.Context, in *ListNodesRequest, opts ...grpc.CallOption) (*ListNodesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListNodesResponse)
|
||||
err := c.cc.Invoke(ctx, McpMasterService_ListNodes_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// McpMasterServiceServer is the server API for McpMasterService service.
|
||||
// All implementations must embed UnimplementedMcpMasterServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// McpMasterService coordinates multi-node deployments. The CLI sends
|
||||
// deploy/undeploy/status requests to the master, which places services on
|
||||
// nodes, forwards to agents, and coordinates edge routing.
|
||||
type McpMasterServiceServer interface {
|
||||
// CLI operations.
|
||||
Deploy(context.Context, *MasterDeployRequest) (*MasterDeployResponse, error)
|
||||
Undeploy(context.Context, *MasterUndeployRequest) (*MasterUndeployResponse, error)
|
||||
Status(context.Context, *MasterStatusRequest) (*MasterStatusResponse, error)
|
||||
ListNodes(context.Context, *ListNodesRequest) (*ListNodesResponse, error)
|
||||
mustEmbedUnimplementedMcpMasterServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedMcpMasterServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedMcpMasterServiceServer struct{}
|
||||
|
||||
func (UnimplementedMcpMasterServiceServer) Deploy(context.Context, *MasterDeployRequest) (*MasterDeployResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Deploy not implemented")
|
||||
}
|
||||
func (UnimplementedMcpMasterServiceServer) Undeploy(context.Context, *MasterUndeployRequest) (*MasterUndeployResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Undeploy not implemented")
|
||||
}
|
||||
func (UnimplementedMcpMasterServiceServer) Status(context.Context, *MasterStatusRequest) (*MasterStatusResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Status not implemented")
|
||||
}
|
||||
func (UnimplementedMcpMasterServiceServer) ListNodes(context.Context, *ListNodesRequest) (*ListNodesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListNodes not implemented")
|
||||
}
|
||||
func (UnimplementedMcpMasterServiceServer) mustEmbedUnimplementedMcpMasterServiceServer() {}
|
||||
func (UnimplementedMcpMasterServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeMcpMasterServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to McpMasterServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeMcpMasterServiceServer interface {
|
||||
mustEmbedUnimplementedMcpMasterServiceServer()
|
||||
}
|
||||
|
||||
func RegisterMcpMasterServiceServer(s grpc.ServiceRegistrar, srv McpMasterServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedMcpMasterServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&McpMasterService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _McpMasterService_Deploy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(MasterDeployRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(McpMasterServiceServer).Deploy(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: McpMasterService_Deploy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(McpMasterServiceServer).Deploy(ctx, req.(*MasterDeployRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _McpMasterService_Undeploy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(MasterUndeployRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(McpMasterServiceServer).Undeploy(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: McpMasterService_Undeploy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(McpMasterServiceServer).Undeploy(ctx, req.(*MasterUndeployRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _McpMasterService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(MasterStatusRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(McpMasterServiceServer).Status(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: McpMasterService_Status_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(McpMasterServiceServer).Status(ctx, req.(*MasterStatusRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _McpMasterService_ListNodes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListNodesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(McpMasterServiceServer).ListNodes(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: McpMasterService_ListNodes_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(McpMasterServiceServer).ListNodes(ctx, req.(*ListNodesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// McpMasterService_ServiceDesc is the grpc.ServiceDesc for McpMasterService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var McpMasterService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "mcp.v1.McpMasterService",
|
||||
HandlerType: (*McpMasterServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Deploy",
|
||||
Handler: _McpMasterService_Deploy_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Undeploy",
|
||||
Handler: _McpMasterService_Undeploy_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Status",
|
||||
Handler: _McpMasterService_Status_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListNodes",
|
||||
Handler: _McpMasterService_ListNodes_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/mcp/v1/master.proto",
|
||||
}
|
||||
1212
gen/mcp/v1/mcp.pb.go
1212
gen/mcp/v1/mcp.pb.go
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,10 @@ const (
|
||||
McpAgentService_ListProxyRoutes_FullMethodName = "/mcp.v1.McpAgentService/ListProxyRoutes"
|
||||
McpAgentService_AddProxyRoute_FullMethodName = "/mcp.v1.McpAgentService/AddProxyRoute"
|
||||
McpAgentService_RemoveProxyRoute_FullMethodName = "/mcp.v1.McpAgentService/RemoveProxyRoute"
|
||||
McpAgentService_SetupEdgeRoute_FullMethodName = "/mcp.v1.McpAgentService/SetupEdgeRoute"
|
||||
McpAgentService_RemoveEdgeRoute_FullMethodName = "/mcp.v1.McpAgentService/RemoveEdgeRoute"
|
||||
McpAgentService_ListEdgeRoutes_FullMethodName = "/mcp.v1.McpAgentService/ListEdgeRoutes"
|
||||
McpAgentService_HealthCheck_FullMethodName = "/mcp.v1.McpAgentService/HealthCheck"
|
||||
McpAgentService_Logs_FullMethodName = "/mcp.v1.McpAgentService/Logs"
|
||||
)
|
||||
|
||||
@@ -71,6 +75,12 @@ type McpAgentServiceClient interface {
|
||||
ListProxyRoutes(ctx context.Context, in *ListProxyRoutesRequest, opts ...grpc.CallOption) (*ListProxyRoutesResponse, error)
|
||||
AddProxyRoute(ctx context.Context, in *AddProxyRouteRequest, opts ...grpc.CallOption) (*AddProxyRouteResponse, error)
|
||||
RemoveProxyRoute(ctx context.Context, in *RemoveProxyRouteRequest, opts ...grpc.CallOption) (*RemoveProxyRouteResponse, error)
|
||||
// Edge routing (called by master on edge nodes)
|
||||
SetupEdgeRoute(ctx context.Context, in *SetupEdgeRouteRequest, opts ...grpc.CallOption) (*SetupEdgeRouteResponse, error)
|
||||
RemoveEdgeRoute(ctx context.Context, in *RemoveEdgeRouteRequest, opts ...grpc.CallOption) (*RemoveEdgeRouteResponse, error)
|
||||
ListEdgeRoutes(ctx context.Context, in *ListEdgeRoutesRequest, opts ...grpc.CallOption) (*ListEdgeRoutesResponse, error)
|
||||
// Health (called by master on missed heartbeats)
|
||||
HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)
|
||||
// Logs
|
||||
Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error)
|
||||
}
|
||||
@@ -263,6 +273,46 @@ func (c *mcpAgentServiceClient) RemoveProxyRoute(ctx context.Context, in *Remove
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mcpAgentServiceClient) SetupEdgeRoute(ctx context.Context, in *SetupEdgeRouteRequest, opts ...grpc.CallOption) (*SetupEdgeRouteResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SetupEdgeRouteResponse)
|
||||
err := c.cc.Invoke(ctx, McpAgentService_SetupEdgeRoute_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mcpAgentServiceClient) RemoveEdgeRoute(ctx context.Context, in *RemoveEdgeRouteRequest, opts ...grpc.CallOption) (*RemoveEdgeRouteResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RemoveEdgeRouteResponse)
|
||||
err := c.cc.Invoke(ctx, McpAgentService_RemoveEdgeRoute_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mcpAgentServiceClient) ListEdgeRoutes(ctx context.Context, in *ListEdgeRoutesRequest, opts ...grpc.CallOption) (*ListEdgeRoutesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListEdgeRoutesResponse)
|
||||
err := c.cc.Invoke(ctx, McpAgentService_ListEdgeRoutes_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mcpAgentServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(HealthCheckResponse)
|
||||
err := c.cc.Invoke(ctx, McpAgentService_HealthCheck_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mcpAgentServiceClient) Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &McpAgentService_ServiceDesc.Streams[0], McpAgentService_Logs_FullMethodName, cOpts...)
|
||||
@@ -313,6 +363,12 @@ type McpAgentServiceServer interface {
|
||||
ListProxyRoutes(context.Context, *ListProxyRoutesRequest) (*ListProxyRoutesResponse, error)
|
||||
AddProxyRoute(context.Context, *AddProxyRouteRequest) (*AddProxyRouteResponse, error)
|
||||
RemoveProxyRoute(context.Context, *RemoveProxyRouteRequest) (*RemoveProxyRouteResponse, error)
|
||||
// Edge routing (called by master on edge nodes)
|
||||
SetupEdgeRoute(context.Context, *SetupEdgeRouteRequest) (*SetupEdgeRouteResponse, error)
|
||||
RemoveEdgeRoute(context.Context, *RemoveEdgeRouteRequest) (*RemoveEdgeRouteResponse, error)
|
||||
ListEdgeRoutes(context.Context, *ListEdgeRoutesRequest) (*ListEdgeRoutesResponse, error)
|
||||
// Health (called by master on missed heartbeats)
|
||||
HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
|
||||
// Logs
|
||||
Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error
|
||||
mustEmbedUnimplementedMcpAgentServiceServer()
|
||||
@@ -379,6 +435,18 @@ func (UnimplementedMcpAgentServiceServer) AddProxyRoute(context.Context, *AddPro
|
||||
func (UnimplementedMcpAgentServiceServer) RemoveProxyRoute(context.Context, *RemoveProxyRouteRequest) (*RemoveProxyRouteResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method RemoveProxyRoute not implemented")
|
||||
}
|
||||
func (UnimplementedMcpAgentServiceServer) SetupEdgeRoute(context.Context, *SetupEdgeRouteRequest) (*SetupEdgeRouteResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetupEdgeRoute not implemented")
|
||||
}
|
||||
func (UnimplementedMcpAgentServiceServer) RemoveEdgeRoute(context.Context, *RemoveEdgeRouteRequest) (*RemoveEdgeRouteResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method RemoveEdgeRoute not implemented")
|
||||
}
|
||||
func (UnimplementedMcpAgentServiceServer) ListEdgeRoutes(context.Context, *ListEdgeRoutesRequest) (*ListEdgeRoutesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListEdgeRoutes not implemented")
|
||||
}
|
||||
func (UnimplementedMcpAgentServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method HealthCheck not implemented")
|
||||
}
|
||||
func (UnimplementedMcpAgentServiceServer) Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error {
|
||||
return status.Error(codes.Unimplemented, "method Logs not implemented")
|
||||
}
|
||||
@@ -727,6 +795,78 @@ func _McpAgentService_RemoveProxyRoute_Handler(srv interface{}, ctx context.Cont
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _McpAgentService_SetupEdgeRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SetupEdgeRouteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(McpAgentServiceServer).SetupEdgeRoute(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: McpAgentService_SetupEdgeRoute_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(McpAgentServiceServer).SetupEdgeRoute(ctx, req.(*SetupEdgeRouteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _McpAgentService_RemoveEdgeRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RemoveEdgeRouteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(McpAgentServiceServer).RemoveEdgeRoute(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: McpAgentService_RemoveEdgeRoute_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(McpAgentServiceServer).RemoveEdgeRoute(ctx, req.(*RemoveEdgeRouteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _McpAgentService_ListEdgeRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListEdgeRoutesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(McpAgentServiceServer).ListEdgeRoutes(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: McpAgentService_ListEdgeRoutes_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(McpAgentServiceServer).ListEdgeRoutes(ctx, req.(*ListEdgeRoutesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _McpAgentService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(HealthCheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(McpAgentServiceServer).HealthCheck(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: McpAgentService_HealthCheck_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(McpAgentServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _McpAgentService_Logs_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(LogsRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
@@ -817,6 +957,22 @@ var McpAgentService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "RemoveProxyRoute",
|
||||
Handler: _McpAgentService_RemoveProxyRoute_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetupEdgeRoute",
|
||||
Handler: _McpAgentService_SetupEdgeRoute_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveEdgeRoute",
|
||||
Handler: _McpAgentService_RemoveEdgeRoute_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListEdgeRoutes",
|
||||
Handler: _McpAgentService_ListEdgeRoutes_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "HealthCheck",
|
||||
Handler: _McpAgentService_HealthCheck_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -134,7 +134,8 @@ func (a *Agent) deployComponent(ctx context.Context, serviceName string, cs *mcp
|
||||
Error: fmt.Sprintf("allocate route ports: %v", err),
|
||||
}
|
||||
}
|
||||
runSpec.Ports = ports
|
||||
// Merge explicit ports from the spec with route-allocated ports.
|
||||
runSpec.Ports = append(cs.GetPorts(), ports...)
|
||||
runSpec.Env = append(runSpec.Env, env...)
|
||||
} else {
|
||||
// Legacy: use ports directly from the spec.
|
||||
|
||||
196
internal/agent/edge_rpc.go
Normal file
196
internal/agent/edge_rpc.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||
mcproxy "git.wntrmute.dev/mc/mc-proxy/client/mcproxy"
|
||||
"git.wntrmute.dev/mc/mcp/internal/registry"
|
||||
)
|
||||
|
||||
// SetupEdgeRoute provisions a TLS cert and registers an mc-proxy route for a
|
||||
// public hostname. Called by the master on edge nodes.
|
||||
func (a *Agent) SetupEdgeRoute(ctx context.Context, req *mcpv1.SetupEdgeRouteRequest) (*mcpv1.SetupEdgeRouteResponse, error) {
|
||||
a.Logger.Info("SetupEdgeRoute", "hostname", req.GetHostname(),
|
||||
"backend_hostname", req.GetBackendHostname(), "backend_port", req.GetBackendPort())
|
||||
|
||||
// Validate required fields.
|
||||
if req.GetHostname() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "hostname is required")
|
||||
}
|
||||
if req.GetBackendHostname() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "backend_hostname is required")
|
||||
}
|
||||
if req.GetBackendPort() == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "backend_port is required")
|
||||
}
|
||||
if !req.GetBackendTls() {
|
||||
return nil, status.Error(codes.InvalidArgument, "backend_tls must be true")
|
||||
}
|
||||
|
||||
if a.Proxy == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "mc-proxy not configured")
|
||||
}
|
||||
|
||||
// Resolve the backend hostname to a Tailnet IP.
|
||||
ips, err := net.LookupHost(req.GetBackendHostname())
|
||||
if err != nil || len(ips) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "cannot resolve backend_hostname %q: %v", req.GetBackendHostname(), err)
|
||||
}
|
||||
backendIP := ips[0]
|
||||
|
||||
// Validate the resolved IP is a Tailnet address (100.64.0.0/10).
|
||||
ip := net.ParseIP(backendIP)
|
||||
if ip == nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "resolved IP %q is not valid", backendIP)
|
||||
}
|
||||
_, tailnet, _ := net.ParseCIDR("100.64.0.0/10")
|
||||
if !tailnet.Contains(ip) {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "resolved IP %s is not a Tailnet address", backendIP)
|
||||
}
|
||||
|
||||
backend := fmt.Sprintf("%s:%d", backendIP, req.GetBackendPort())
|
||||
|
||||
// Provision TLS cert for the public hostname if cert provisioner is available.
|
||||
certPath := ""
|
||||
keyPath := ""
|
||||
if a.Certs != nil {
|
||||
if err := a.Certs.EnsureCert(ctx, req.GetHostname(), []string{req.GetHostname()}); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "provision cert for %s: %v", req.GetHostname(), err)
|
||||
}
|
||||
certPath = a.Proxy.CertPath(req.GetHostname())
|
||||
keyPath = a.Proxy.KeyPath(req.GetHostname())
|
||||
} else {
|
||||
// No cert provisioner — check if certs already exist on disk.
|
||||
certPath = a.Proxy.CertPath(req.GetHostname())
|
||||
keyPath = a.Proxy.KeyPath(req.GetHostname())
|
||||
if _, err := os.Stat(certPath); err != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "no cert provisioner and cert not found at %s", certPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Register the L7 route in mc-proxy.
|
||||
route := mcproxy.Route{
|
||||
Hostname: req.GetHostname(),
|
||||
Backend: backend,
|
||||
Mode: "l7",
|
||||
TLSCert: certPath,
|
||||
TLSKey: keyPath,
|
||||
BackendTLS: true,
|
||||
}
|
||||
if err := a.Proxy.AddRoute(ctx, ":443", route); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "add mc-proxy route: %v", err)
|
||||
}
|
||||
|
||||
// Persist the edge route in the registry.
|
||||
if err := registry.CreateEdgeRoute(a.DB, req.GetHostname(), req.GetBackendHostname(), int(req.GetBackendPort()), certPath, keyPath); err != nil {
|
||||
a.Logger.Warn("failed to persist edge route", "hostname", req.GetHostname(), "err", err)
|
||||
}
|
||||
|
||||
a.Logger.Info("edge route established",
|
||||
"hostname", req.GetHostname(), "backend", backend, "cert", certPath)
|
||||
|
||||
return &mcpv1.SetupEdgeRouteResponse{}, nil
|
||||
}
|
||||
|
||||
// RemoveEdgeRoute removes an mc-proxy route and cleans up the TLS cert for a
|
||||
// public hostname. Called by the master on edge nodes.
|
||||
func (a *Agent) RemoveEdgeRoute(ctx context.Context, req *mcpv1.RemoveEdgeRouteRequest) (*mcpv1.RemoveEdgeRouteResponse, error) {
|
||||
a.Logger.Info("RemoveEdgeRoute", "hostname", req.GetHostname())
|
||||
|
||||
if req.GetHostname() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "hostname is required")
|
||||
}
|
||||
|
||||
if a.Proxy == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "mc-proxy not configured")
|
||||
}
|
||||
|
||||
// Remove the mc-proxy route.
|
||||
if err := a.Proxy.RemoveRoute(ctx, ":443", req.GetHostname()); err != nil {
|
||||
a.Logger.Warn("remove mc-proxy route", "hostname", req.GetHostname(), "err", err)
|
||||
// Continue — clean up cert and registry even if route removal fails.
|
||||
}
|
||||
|
||||
// Remove the TLS cert.
|
||||
if a.Certs != nil {
|
||||
if err := a.Certs.RemoveCert(req.GetHostname()); err != nil {
|
||||
a.Logger.Warn("remove cert", "hostname", req.GetHostname(), "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from registry.
|
||||
if err := registry.DeleteEdgeRoute(a.DB, req.GetHostname()); err != nil {
|
||||
a.Logger.Warn("delete edge route from registry", "hostname", req.GetHostname(), "err", err)
|
||||
}
|
||||
|
||||
a.Logger.Info("edge route removed", "hostname", req.GetHostname())
|
||||
return &mcpv1.RemoveEdgeRouteResponse{}, nil
|
||||
}
|
||||
|
||||
// ListEdgeRoutes returns all edge routes managed by this agent.
|
||||
func (a *Agent) ListEdgeRoutes(_ context.Context, _ *mcpv1.ListEdgeRoutesRequest) (*mcpv1.ListEdgeRoutesResponse, error) {
|
||||
a.Logger.Debug("ListEdgeRoutes called")
|
||||
|
||||
routes, err := registry.ListEdgeRoutes(a.DB)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "list edge routes: %v", err)
|
||||
}
|
||||
|
||||
resp := &mcpv1.ListEdgeRoutesResponse{}
|
||||
for _, r := range routes {
|
||||
er := &mcpv1.EdgeRoute{
|
||||
Hostname: r.Hostname,
|
||||
BackendHostname: r.BackendHostname,
|
||||
BackendPort: int32(r.BackendPort), //nolint:gosec // port is a small positive integer
|
||||
}
|
||||
|
||||
// Read cert metadata if available.
|
||||
if r.TLSCert != "" {
|
||||
if certData, readErr := os.ReadFile(r.TLSCert); readErr == nil { //nolint:gosec // path from registry, not user input
|
||||
if block, _ := pem.Decode(certData); block != nil {
|
||||
if cert, parseErr := x509.ParseCertificate(block.Bytes); parseErr == nil {
|
||||
er.CertSerial = cert.SerialNumber.String()
|
||||
er.CertExpires = cert.NotAfter.UTC().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.Routes = append(resp.Routes, er)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// HealthCheck returns the agent's health status. Called by the master when
|
||||
// heartbeats are missed.
|
||||
func (a *Agent) HealthCheck(_ context.Context, _ *mcpv1.HealthCheckRequest) (*mcpv1.HealthCheckResponse, error) {
|
||||
a.Logger.Debug("HealthCheck called")
|
||||
|
||||
st := "healthy"
|
||||
containers := int32(0)
|
||||
|
||||
// Count running containers if the runtime is available.
|
||||
if a.Runtime != nil {
|
||||
if list, err := a.Runtime.List(context.Background()); err == nil {
|
||||
containers = int32(len(list)) //nolint:gosec // container count is small
|
||||
} else {
|
||||
st = "degraded"
|
||||
}
|
||||
}
|
||||
|
||||
return &mcpv1.HealthCheckResponse{
|
||||
Status: st,
|
||||
Containers: containers,
|
||||
}, nil
|
||||
}
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// StopService stops all components of a service.
|
||||
// StopService stops all components of a service, or a single component if specified.
|
||||
func (a *Agent) StopService(ctx context.Context, req *mcpv1.StopServiceRequest) (*mcpv1.StopServiceResponse, error) {
|
||||
a.Logger.Info("StopService", "service", req.GetName())
|
||||
a.Logger.Info("StopService", "service", req.GetName(), "component", req.GetComponent())
|
||||
|
||||
if req.GetName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "service name is required")
|
||||
@@ -25,6 +25,13 @@ func (a *Agent) StopService(ctx context.Context, req *mcpv1.StopServiceRequest)
|
||||
return nil, status.Errorf(codes.Internal, "list components: %v", err)
|
||||
}
|
||||
|
||||
if target := req.GetComponent(); target != "" {
|
||||
components, err = filterComponents(components, req.GetName(), target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var results []*mcpv1.ComponentResult
|
||||
for _, c := range components {
|
||||
containerName := ContainerNameFor(req.GetName(), c.Name)
|
||||
@@ -59,10 +66,10 @@ func (a *Agent) StopService(ctx context.Context, req *mcpv1.StopServiceRequest)
|
||||
return &mcpv1.StopServiceResponse{Results: results}, nil
|
||||
}
|
||||
|
||||
// StartService starts all components of a service. If a container already
|
||||
// exists but is stopped, it is removed first so a fresh one can be created.
|
||||
// StartService starts all components of a service, or a single component if specified.
|
||||
// If a container already exists but is stopped, it is removed first so a fresh one can be created.
|
||||
func (a *Agent) StartService(ctx context.Context, req *mcpv1.StartServiceRequest) (*mcpv1.StartServiceResponse, error) {
|
||||
a.Logger.Info("StartService", "service", req.GetName())
|
||||
a.Logger.Info("StartService", "service", req.GetName(), "component", req.GetComponent())
|
||||
|
||||
if req.GetName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "service name is required")
|
||||
@@ -73,6 +80,13 @@ func (a *Agent) StartService(ctx context.Context, req *mcpv1.StartServiceRequest
|
||||
return nil, status.Errorf(codes.Internal, "list components: %v", err)
|
||||
}
|
||||
|
||||
if target := req.GetComponent(); target != "" {
|
||||
components, err = filterComponents(components, req.GetName(), target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var results []*mcpv1.ComponentResult
|
||||
for _, c := range components {
|
||||
r := startComponent(ctx, a, req.GetName(), &c)
|
||||
@@ -82,10 +96,10 @@ func (a *Agent) StartService(ctx context.Context, req *mcpv1.StartServiceRequest
|
||||
return &mcpv1.StartServiceResponse{Results: results}, nil
|
||||
}
|
||||
|
||||
// RestartService restarts all components of a service by stopping, removing,
|
||||
// and re-creating each container. The desired_state is not changed.
|
||||
// RestartService restarts all components of a service, or a single component if specified,
|
||||
// by stopping, removing, and re-creating each container. The desired_state is not changed.
|
||||
func (a *Agent) RestartService(ctx context.Context, req *mcpv1.RestartServiceRequest) (*mcpv1.RestartServiceResponse, error) {
|
||||
a.Logger.Info("RestartService", "service", req.GetName())
|
||||
a.Logger.Info("RestartService", "service", req.GetName(), "component", req.GetComponent())
|
||||
|
||||
if req.GetName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "service name is required")
|
||||
@@ -96,6 +110,13 @@ func (a *Agent) RestartService(ctx context.Context, req *mcpv1.RestartServiceReq
|
||||
return nil, status.Errorf(codes.Internal, "list components: %v", err)
|
||||
}
|
||||
|
||||
if target := req.GetComponent(); target != "" {
|
||||
components, err = filterComponents(components, req.GetName(), target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var results []*mcpv1.ComponentResult
|
||||
for _, c := range components {
|
||||
r := restartComponent(ctx, a, req.GetName(), &c)
|
||||
@@ -167,6 +188,16 @@ func componentToSpec(service string, c *registry.Component) runtime.ContainerSpe
|
||||
}
|
||||
}
|
||||
|
||||
// filterComponents returns only the component matching target, or an error if not found.
|
||||
func filterComponents(components []registry.Component, service, target string) ([]registry.Component, error) {
|
||||
for _, c := range components {
|
||||
if c.Name == target {
|
||||
return []registry.Component{c}, nil
|
||||
}
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "component %q not found in service %q", target, service)
|
||||
}
|
||||
|
||||
// componentExists checks whether a component already exists in the registry.
|
||||
func componentExists(db *sql.DB, service, name string) bool {
|
||||
_, err := registry.GetComponent(db, service, name)
|
||||
|
||||
@@ -48,6 +48,16 @@ func (p *ProxyRouter) Close() error {
|
||||
return p.client.Close()
|
||||
}
|
||||
|
||||
// CertPath returns the expected TLS certificate path for a given name.
|
||||
func (p *ProxyRouter) CertPath(name string) string {
|
||||
return filepath.Join(p.certDir, name+".pem")
|
||||
}
|
||||
|
||||
// KeyPath returns the expected TLS key path for a given name.
|
||||
func (p *ProxyRouter) KeyPath(name string) string {
|
||||
return filepath.Join(p.certDir, name+".key")
|
||||
}
|
||||
|
||||
// GetStatus returns the mc-proxy server status.
|
||||
func (p *ProxyRouter) GetStatus(ctx context.Context) (*mcproxy.Status, error) {
|
||||
if p == nil {
|
||||
|
||||
@@ -69,6 +69,8 @@ func (a *Agent) AddProxyRoute(ctx context.Context, req *mcpv1.AddProxyRouteReque
|
||||
Backend: req.GetBackend(),
|
||||
Mode: req.GetMode(),
|
||||
BackendTLS: req.GetBackendTls(),
|
||||
TLSCert: req.GetTlsCert(),
|
||||
TLSKey: req.GetTlsKey(),
|
||||
}
|
||||
|
||||
if err := a.Proxy.AddRoute(ctx, req.GetListenerAddr(), route); err != nil {
|
||||
|
||||
@@ -10,11 +10,19 @@ 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"`
|
||||
Services ServicesConfig `toml:"services"`
|
||||
Build BuildConfig `toml:"build"`
|
||||
MCIAS MCIASConfig `toml:"mcias"`
|
||||
Auth AuthConfig `toml:"auth"`
|
||||
Nodes []NodeConfig `toml:"nodes"`
|
||||
Master *CLIMasterConfig `toml:"master,omitempty"`
|
||||
}
|
||||
|
||||
// CLIMasterConfig holds the optional master connection settings.
|
||||
// When configured, deploy/undeploy/status go through the master
|
||||
// instead of directly to agents.
|
||||
type CLIMasterConfig struct {
|
||||
Address string `toml:"address"` // master gRPC address (e.g. "100.95.252.120:9555")
|
||||
}
|
||||
|
||||
// BuildConfig holds settings for building container images.
|
||||
|
||||
168
internal/config/master.go
Normal file
168
internal/config/master.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// MasterConfig is the configuration for the mcp-master daemon.
|
||||
type MasterConfig struct {
|
||||
Server ServerConfig `toml:"server"`
|
||||
Database DatabaseConfig `toml:"database"`
|
||||
MCIAS MCIASConfig `toml:"mcias"`
|
||||
Edge EdgeConfig `toml:"edge"`
|
||||
Registration RegistrationConfig `toml:"registration"`
|
||||
Timeouts TimeoutsConfig `toml:"timeouts"`
|
||||
MCNS MCNSConfig `toml:"mcns"`
|
||||
Log LogConfig `toml:"log"`
|
||||
Nodes []MasterNodeConfig `toml:"nodes"`
|
||||
|
||||
// Master holds the master's own MCIAS service token for dialing agents.
|
||||
Master MasterSettings `toml:"master"`
|
||||
}
|
||||
|
||||
// MasterSettings holds settings specific to the master's own identity.
|
||||
type MasterSettings struct {
|
||||
// ServiceTokenPath is the path to the MCIAS service token file
|
||||
// used by the master to authenticate to agents.
|
||||
ServiceTokenPath string `toml:"service_token_path"`
|
||||
|
||||
// CACert is the path to the CA certificate for verifying agent TLS.
|
||||
CACert string `toml:"ca_cert"`
|
||||
}
|
||||
|
||||
// EdgeConfig holds settings for edge route management.
|
||||
type EdgeConfig struct {
|
||||
// AllowedDomains is the list of domains that public hostnames
|
||||
// must fall under. Validation uses proper domain label matching.
|
||||
AllowedDomains []string `toml:"allowed_domains"`
|
||||
}
|
||||
|
||||
// RegistrationConfig holds agent registration settings.
|
||||
type RegistrationConfig struct {
|
||||
// AllowedAgents is the list of MCIAS service identities permitted
|
||||
// to register with the master (e.g., "agent-rift", "agent-svc").
|
||||
AllowedAgents []string `toml:"allowed_agents"`
|
||||
|
||||
// MaxNodes is the maximum number of registered nodes.
|
||||
MaxNodes int `toml:"max_nodes"`
|
||||
}
|
||||
|
||||
// TimeoutsConfig holds timeout durations for master operations.
|
||||
type TimeoutsConfig struct {
|
||||
Deploy Duration `toml:"deploy"`
|
||||
EdgeRoute Duration `toml:"edge_route"`
|
||||
HealthCheck Duration `toml:"health_check"`
|
||||
Undeploy Duration `toml:"undeploy"`
|
||||
Snapshot Duration `toml:"snapshot"`
|
||||
}
|
||||
|
||||
// MasterNodeConfig is a bootstrap node entry in the master config.
|
||||
type MasterNodeConfig struct {
|
||||
Name string `toml:"name"`
|
||||
Address string `toml:"address"`
|
||||
Role string `toml:"role"` // "worker", "edge", or "master"
|
||||
}
|
||||
|
||||
// LoadMasterConfig reads and validates a master configuration file.
|
||||
func LoadMasterConfig(path string) (*MasterConfig, error) {
|
||||
data, err := os.ReadFile(path) //nolint:gosec // config path from trusted CLI flag
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config %q: %w", path, err)
|
||||
}
|
||||
|
||||
var cfg MasterConfig
|
||||
if err := toml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config %q: %w", path, err)
|
||||
}
|
||||
|
||||
applyMasterDefaults(&cfg)
|
||||
applyMasterEnvOverrides(&cfg)
|
||||
|
||||
if err := validateMasterConfig(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("validate config: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func applyMasterDefaults(cfg *MasterConfig) {
|
||||
if cfg.Log.Level == "" {
|
||||
cfg.Log.Level = "info"
|
||||
}
|
||||
if cfg.Registration.MaxNodes == 0 {
|
||||
cfg.Registration.MaxNodes = 16
|
||||
}
|
||||
if cfg.Timeouts.Deploy.Duration == 0 {
|
||||
cfg.Timeouts.Deploy.Duration = 5 * time.Minute
|
||||
}
|
||||
if cfg.Timeouts.EdgeRoute.Duration == 0 {
|
||||
cfg.Timeouts.EdgeRoute.Duration = 30 * time.Second
|
||||
}
|
||||
if cfg.Timeouts.HealthCheck.Duration == 0 {
|
||||
cfg.Timeouts.HealthCheck.Duration = 5 * time.Second
|
||||
}
|
||||
if cfg.Timeouts.Undeploy.Duration == 0 {
|
||||
cfg.Timeouts.Undeploy.Duration = 2 * time.Minute
|
||||
}
|
||||
if cfg.Timeouts.Snapshot.Duration == 0 {
|
||||
cfg.Timeouts.Snapshot.Duration = 10 * time.Minute
|
||||
}
|
||||
if cfg.MCNS.Zone == "" {
|
||||
cfg.MCNS.Zone = "svc.mcp.metacircular.net"
|
||||
}
|
||||
for i := range cfg.Nodes {
|
||||
if cfg.Nodes[i].Role == "" {
|
||||
cfg.Nodes[i].Role = "worker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyMasterEnvOverrides(cfg *MasterConfig) {
|
||||
if v := os.Getenv("MCP_MASTER_SERVER_GRPC_ADDR"); v != "" {
|
||||
cfg.Server.GRPCAddr = v
|
||||
}
|
||||
if v := os.Getenv("MCP_MASTER_SERVER_TLS_CERT"); v != "" {
|
||||
cfg.Server.TLSCert = v
|
||||
}
|
||||
if v := os.Getenv("MCP_MASTER_SERVER_TLS_KEY"); v != "" {
|
||||
cfg.Server.TLSKey = v
|
||||
}
|
||||
if v := os.Getenv("MCP_MASTER_DATABASE_PATH"); v != "" {
|
||||
cfg.Database.Path = v
|
||||
}
|
||||
if v := os.Getenv("MCP_MASTER_LOG_LEVEL"); v != "" {
|
||||
cfg.Log.Level = v
|
||||
}
|
||||
}
|
||||
|
||||
func validateMasterConfig(cfg *MasterConfig) error {
|
||||
if cfg.Server.GRPCAddr == "" {
|
||||
return fmt.Errorf("server.grpc_addr is required")
|
||||
}
|
||||
if cfg.Server.TLSCert == "" {
|
||||
return fmt.Errorf("server.tls_cert is required")
|
||||
}
|
||||
if cfg.Server.TLSKey == "" {
|
||||
return fmt.Errorf("server.tls_key is required")
|
||||
}
|
||||
if cfg.Database.Path == "" {
|
||||
return fmt.Errorf("database.path is required")
|
||||
}
|
||||
if cfg.MCIAS.ServerURL == "" {
|
||||
return fmt.Errorf("mcias.server_url is required")
|
||||
}
|
||||
if cfg.MCIAS.ServiceName == "" {
|
||||
return fmt.Errorf("mcias.service_name is required")
|
||||
}
|
||||
if len(cfg.Nodes) == 0 {
|
||||
return fmt.Errorf("at least one [[nodes]] entry is required")
|
||||
}
|
||||
if cfg.Master.ServiceTokenPath == "" {
|
||||
return fmt.Errorf("master.service_token_path is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
190
internal/master/agentclient.go
Normal file
190
internal/master/agentclient.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// Package master implements the mcp-master orchestrator.
|
||||
package master
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// AgentClient wraps a gRPC connection to a single mcp-agent.
|
||||
type AgentClient struct {
|
||||
conn *grpc.ClientConn
|
||||
client mcpv1.McpAgentServiceClient
|
||||
Node string
|
||||
}
|
||||
|
||||
// DialAgent connects to an agent at the given address using TLS 1.3.
|
||||
// The token is attached to every outgoing RPC via metadata.
|
||||
func DialAgent(address, caCertPath, token string) (*AgentClient, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
if caCertPath != "" {
|
||||
caCert, err := os.ReadFile(caCertPath) //nolint:gosec // trusted config path
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read CA cert %q: %w", caCertPath, err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("invalid CA cert %q", caCertPath)
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(
|
||||
address,
|
||||
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
||||
grpc.WithUnaryInterceptor(agentTokenInterceptor(token)),
|
||||
grpc.WithStreamInterceptor(agentStreamTokenInterceptor(token)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial agent %q: %w", address, err)
|
||||
}
|
||||
|
||||
return &AgentClient{
|
||||
conn: conn,
|
||||
client: mcpv1.NewMcpAgentServiceClient(conn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying gRPC connection.
|
||||
func (c *AgentClient) Close() error {
|
||||
if c == nil || c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// Deploy forwards a deploy request to the agent.
|
||||
func (c *AgentClient) Deploy(ctx context.Context, req *mcpv1.DeployRequest) (*mcpv1.DeployResponse, error) {
|
||||
return c.client.Deploy(ctx, req)
|
||||
}
|
||||
|
||||
// UndeployService forwards an undeploy request to the agent.
|
||||
func (c *AgentClient) UndeployService(ctx context.Context, req *mcpv1.UndeployServiceRequest) (*mcpv1.UndeployServiceResponse, error) {
|
||||
return c.client.UndeployService(ctx, req)
|
||||
}
|
||||
|
||||
// GetServiceStatus queries a service's status on the agent.
|
||||
func (c *AgentClient) GetServiceStatus(ctx context.Context, req *mcpv1.GetServiceStatusRequest) (*mcpv1.GetServiceStatusResponse, error) {
|
||||
return c.client.GetServiceStatus(ctx, req)
|
||||
}
|
||||
|
||||
// ListServices lists all services on the agent.
|
||||
func (c *AgentClient) ListServices(ctx context.Context, req *mcpv1.ListServicesRequest) (*mcpv1.ListServicesResponse, error) {
|
||||
return c.client.ListServices(ctx, req)
|
||||
}
|
||||
|
||||
// SetupEdgeRoute sets up an edge route on the agent.
|
||||
func (c *AgentClient) SetupEdgeRoute(ctx context.Context, req *mcpv1.SetupEdgeRouteRequest) (*mcpv1.SetupEdgeRouteResponse, error) {
|
||||
return c.client.SetupEdgeRoute(ctx, req)
|
||||
}
|
||||
|
||||
// RemoveEdgeRoute removes an edge route from the agent.
|
||||
func (c *AgentClient) RemoveEdgeRoute(ctx context.Context, req *mcpv1.RemoveEdgeRouteRequest) (*mcpv1.RemoveEdgeRouteResponse, error) {
|
||||
return c.client.RemoveEdgeRoute(ctx, req)
|
||||
}
|
||||
|
||||
// ListEdgeRoutes lists edge routes on the agent.
|
||||
func (c *AgentClient) ListEdgeRoutes(ctx context.Context, req *mcpv1.ListEdgeRoutesRequest) (*mcpv1.ListEdgeRoutesResponse, error) {
|
||||
return c.client.ListEdgeRoutes(ctx, req)
|
||||
}
|
||||
|
||||
// HealthCheck checks the agent's health.
|
||||
func (c *AgentClient) HealthCheck(ctx context.Context, req *mcpv1.HealthCheckRequest) (*mcpv1.HealthCheckResponse, error) {
|
||||
return c.client.HealthCheck(ctx, req)
|
||||
}
|
||||
|
||||
// agentTokenInterceptor attaches the bearer token to outgoing RPCs.
|
||||
func agentTokenInterceptor(token string) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func agentStreamTokenInterceptor(token string) grpc.StreamClientInterceptor {
|
||||
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
||||
return streamer(ctx, desc, cc, method, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// AgentPool manages connections to multiple agents, keyed by node name.
|
||||
type AgentPool struct {
|
||||
mu sync.RWMutex
|
||||
clients map[string]*AgentClient
|
||||
caCert string
|
||||
token string
|
||||
}
|
||||
|
||||
// NewAgentPool creates a pool with the given CA cert and service token.
|
||||
func NewAgentPool(caCertPath, token string) *AgentPool {
|
||||
return &AgentPool{
|
||||
clients: make(map[string]*AgentClient),
|
||||
caCert: caCertPath,
|
||||
token: token,
|
||||
}
|
||||
}
|
||||
|
||||
// AddNode dials an agent and adds it to the pool.
|
||||
func (p *AgentPool) AddNode(name, address string) error {
|
||||
client, err := DialAgent(address, p.caCert, p.token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add node %s: %w", name, err)
|
||||
}
|
||||
client.Node = name
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Close existing connection if re-adding.
|
||||
if old, ok := p.clients[name]; ok {
|
||||
_ = old.Close()
|
||||
}
|
||||
p.clients[name] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the agent client for a node.
|
||||
func (p *AgentPool) Get(name string) (*AgentClient, error) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
client, ok := p.clients[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("node %q not found in pool", name)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Close closes all agent connections.
|
||||
func (p *AgentPool) Close() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
for _, c := range p.clients {
|
||||
_ = c.Close()
|
||||
}
|
||||
p.clients = make(map[string]*AgentClient)
|
||||
}
|
||||
|
||||
// LoadServiceToken reads a token from a file path.
|
||||
func LoadServiceToken(path string) (string, error) {
|
||||
data, err := os.ReadFile(path) //nolint:gosec // trusted config path
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read service token %q: %w", path, err)
|
||||
}
|
||||
return strings.TrimSpace(string(data)), nil
|
||||
}
|
||||
211
internal/master/deploy.go
Normal file
211
internal/master/deploy.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package master
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||
"git.wntrmute.dev/mc/mcp/internal/masterdb"
|
||||
)
|
||||
|
||||
// Deploy handles the MasterDeployRequest: places the service, forwards to
|
||||
// the agent, registers DNS, and coordinates edge routing.
|
||||
func (m *Master) Deploy(ctx context.Context, req *mcpv1.MasterDeployRequest) (*mcpv1.MasterDeployResponse, error) {
|
||||
spec := req.GetService()
|
||||
if spec == nil || spec.GetName() == "" {
|
||||
return nil, fmt.Errorf("service spec with name is required")
|
||||
}
|
||||
|
||||
serviceName := spec.GetName()
|
||||
tier := spec.GetTier()
|
||||
if tier == "" {
|
||||
tier = "worker"
|
||||
}
|
||||
|
||||
m.Logger.Info("Deploy", "service", serviceName, "tier", tier, "node_override", spec.GetNode())
|
||||
|
||||
resp := &mcpv1.MasterDeployResponse{}
|
||||
|
||||
// Step 1: Place service.
|
||||
nodeName := spec.GetNode()
|
||||
if nodeName == "" {
|
||||
var err error
|
||||
switch tier {
|
||||
case "core":
|
||||
nodeName, err = FindMasterNode(m.DB)
|
||||
default:
|
||||
nodeName, err = PickNode(m.DB)
|
||||
}
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("placement failed: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
resp.Node = nodeName
|
||||
|
||||
node, err := masterdb.GetNode(m.DB, nodeName)
|
||||
if err != nil || node == nil {
|
||||
resp.Error = fmt.Sprintf("node %q not found", nodeName)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Parse the node's Tailnet IP from its address (host:port).
|
||||
nodeHost, _, err := net.SplitHostPort(node.Address)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("invalid node address %q: %v", node.Address, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Step 2: Forward deploy to the agent.
|
||||
client, err := m.Pool.Get(nodeName)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("agent connection: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
deployCtx, deployCancel := context.WithTimeout(ctx, m.Config.Timeouts.Deploy.Duration)
|
||||
defer deployCancel()
|
||||
|
||||
deployResp, err := client.Deploy(deployCtx, &mcpv1.DeployRequest{
|
||||
Service: spec,
|
||||
})
|
||||
if err != nil {
|
||||
resp.DeployResult = &mcpv1.StepResult{Step: "deploy", Error: err.Error()}
|
||||
resp.Error = fmt.Sprintf("agent deploy failed: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
resp.DeployResult = &mcpv1.StepResult{Step: "deploy", Success: true}
|
||||
|
||||
// Check agent-side results for failures.
|
||||
for _, cr := range deployResp.GetResults() {
|
||||
if !cr.GetSuccess() {
|
||||
resp.DeployResult.Success = false
|
||||
resp.DeployResult.Error = fmt.Sprintf("component %s: %s", cr.GetName(), cr.GetError())
|
||||
resp.Error = resp.DeployResult.Error
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Register DNS — Tailnet IP from the node address.
|
||||
if m.DNS != nil {
|
||||
if err := m.DNS.EnsureRecord(ctx, serviceName, nodeHost); err != nil {
|
||||
m.Logger.Warn("DNS registration failed", "service", serviceName, "err", err)
|
||||
resp.DnsResult = &mcpv1.StepResult{Step: "dns", Error: err.Error()}
|
||||
} else {
|
||||
resp.DnsResult = &mcpv1.StepResult{Step: "dns", Success: true}
|
||||
}
|
||||
}
|
||||
|
||||
// Record placement.
|
||||
if err := masterdb.CreatePlacement(m.DB, serviceName, nodeName, tier); err != nil {
|
||||
m.Logger.Error("record placement", "service", serviceName, "err", err)
|
||||
}
|
||||
|
||||
// Steps 4-9: Detect public routes and coordinate edge routing.
|
||||
edgeResult := m.setupEdgeRoutes(ctx, spec, serviceName, nodeHost)
|
||||
if edgeResult != nil {
|
||||
resp.EdgeRouteResult = edgeResult
|
||||
}
|
||||
|
||||
// Compute overall success.
|
||||
resp.Success = true
|
||||
if resp.DeployResult != nil && !resp.DeployResult.Success {
|
||||
resp.Success = false
|
||||
}
|
||||
if resp.EdgeRouteResult != nil && !resp.EdgeRouteResult.Success {
|
||||
resp.Success = false
|
||||
}
|
||||
|
||||
m.Logger.Info("deploy complete", "service", serviceName, "node", nodeName, "success", resp.Success)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// setupEdgeRoutes detects public routes and coordinates edge routing.
|
||||
func (m *Master) setupEdgeRoutes(ctx context.Context, spec *mcpv1.ServiceSpec, serviceName, nodeHost string) *mcpv1.StepResult {
|
||||
var publicRoutes []*mcpv1.RouteSpec
|
||||
for _, comp := range spec.GetComponents() {
|
||||
for _, route := range comp.GetRoutes() {
|
||||
if route.GetPublic() && route.GetHostname() != "" {
|
||||
publicRoutes = append(publicRoutes, route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(publicRoutes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the edge node.
|
||||
edgeNodeName, err := FindEdgeNode(m.DB)
|
||||
if err != nil {
|
||||
return &mcpv1.StepResult{Step: "edge_route", Error: fmt.Sprintf("no edge node: %v", err)}
|
||||
}
|
||||
|
||||
edgeClient, err := m.Pool.Get(edgeNodeName)
|
||||
if err != nil {
|
||||
return &mcpv1.StepResult{Step: "edge_route", Error: fmt.Sprintf("edge agent connection: %v", err)}
|
||||
}
|
||||
|
||||
var lastErr string
|
||||
for _, route := range publicRoutes {
|
||||
hostname := route.GetHostname()
|
||||
|
||||
// Validate hostname against allowed domains.
|
||||
if !m.isAllowedDomain(hostname) {
|
||||
lastErr = fmt.Sprintf("hostname %q not under an allowed domain", hostname)
|
||||
m.Logger.Warn("edge route rejected", "hostname", hostname, "reason", lastErr)
|
||||
continue
|
||||
}
|
||||
|
||||
// Construct the backend hostname: <component>.svc.mcp.<zone>
|
||||
// For simplicity, use the service name as the component name.
|
||||
zone := "metacircular.net"
|
||||
if m.DNS != nil && m.DNS.Zone() != "" {
|
||||
zone = m.DNS.Zone()
|
||||
}
|
||||
backendHostname := serviceName + "." + zone
|
||||
|
||||
edgeCtx, edgeCancel := context.WithTimeout(ctx, m.Config.Timeouts.EdgeRoute.Duration)
|
||||
_, setupErr := edgeClient.SetupEdgeRoute(edgeCtx, &mcpv1.SetupEdgeRouteRequest{
|
||||
Hostname: hostname,
|
||||
BackendHostname: backendHostname,
|
||||
BackendPort: route.GetPort(),
|
||||
BackendTls: true,
|
||||
})
|
||||
edgeCancel()
|
||||
|
||||
if setupErr != nil {
|
||||
lastErr = fmt.Sprintf("setup edge route %s: %v", hostname, setupErr)
|
||||
m.Logger.Warn("edge route setup failed", "hostname", hostname, "err", setupErr)
|
||||
continue
|
||||
}
|
||||
|
||||
// Record edge route in master DB.
|
||||
if dbErr := masterdb.CreateEdgeRoute(m.DB, hostname, serviceName, edgeNodeName, backendHostname, int(route.GetPort())); dbErr != nil {
|
||||
m.Logger.Warn("record edge route", "hostname", hostname, "err", dbErr)
|
||||
}
|
||||
|
||||
m.Logger.Info("edge route established", "hostname", hostname, "edge_node", edgeNodeName)
|
||||
}
|
||||
|
||||
if lastErr != "" {
|
||||
return &mcpv1.StepResult{Step: "edge_route", Error: lastErr}
|
||||
}
|
||||
return &mcpv1.StepResult{Step: "edge_route", Success: true}
|
||||
}
|
||||
|
||||
// isAllowedDomain checks if hostname falls under one of the configured
|
||||
// allowed domains using proper domain label matching.
|
||||
func (m *Master) isAllowedDomain(hostname string) bool {
|
||||
if len(m.Config.Edge.AllowedDomains) == 0 {
|
||||
return true // no restrictions configured
|
||||
}
|
||||
for _, domain := range m.Config.Edge.AllowedDomains {
|
||||
if hostname == domain || strings.HasSuffix(hostname, "."+domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
252
internal/master/dns.go
Normal file
252
internal/master/dns.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package master
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/mc/mcp/internal/auth"
|
||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||
)
|
||||
|
||||
// DNSClient creates and removes A records in MCNS. Unlike the agent's
|
||||
// DNSRegistrar, the master registers records for different node IPs
|
||||
// (the nodeAddr is a per-call parameter, not a fixed config value).
|
||||
type DNSClient struct {
|
||||
serverURL string
|
||||
token string
|
||||
zone string
|
||||
httpClient *http.Client
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
type dnsRecord struct {
|
||||
ID int `json:"ID"`
|
||||
Name string `json:"Name"`
|
||||
Type string `json:"Type"`
|
||||
Value string `json:"Value"`
|
||||
TTL int `json:"TTL"`
|
||||
}
|
||||
|
||||
// NewDNSClient creates a DNS client. Returns (nil, nil) if serverURL is empty.
|
||||
func NewDNSClient(cfg config.MCNSConfig, logger *slog.Logger) (*DNSClient, error) {
|
||||
if cfg.ServerURL == "" {
|
||||
logger.Info("mcns not configured, DNS registration disabled")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
token, err := auth.LoadToken(cfg.TokenPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load mcns token: %w", err)
|
||||
}
|
||||
|
||||
httpClient, err := newHTTPClient(cfg.CACert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create mcns HTTP client: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("master DNS client enabled", "server", cfg.ServerURL, "zone", cfg.Zone)
|
||||
return &DNSClient{
|
||||
serverURL: strings.TrimRight(cfg.ServerURL, "/"),
|
||||
token: token,
|
||||
zone: cfg.Zone,
|
||||
httpClient: httpClient,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Zone returns the configured DNS zone.
|
||||
func (d *DNSClient) Zone() string {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
return d.zone
|
||||
}
|
||||
|
||||
// EnsureRecord ensures an A record exists for serviceName pointing to nodeAddr.
|
||||
func (d *DNSClient) EnsureRecord(ctx context.Context, serviceName, nodeAddr string) error {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
existing, err := d.listRecords(ctx, serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list DNS records: %w", err)
|
||||
}
|
||||
|
||||
for _, r := range existing {
|
||||
if r.Value == nodeAddr {
|
||||
d.logger.Debug("DNS record exists", "service", serviceName, "value", r.Value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(existing) > 0 {
|
||||
d.logger.Info("updating DNS record", "service", serviceName,
|
||||
"old_value", existing[0].Value, "new_value", nodeAddr)
|
||||
return d.updateRecord(ctx, existing[0].ID, serviceName, nodeAddr)
|
||||
}
|
||||
|
||||
d.logger.Info("creating DNS record", "service", serviceName,
|
||||
"record", serviceName+"."+d.zone, "value", nodeAddr)
|
||||
return d.createRecord(ctx, serviceName, nodeAddr)
|
||||
}
|
||||
|
||||
// RemoveRecord removes A records for serviceName.
|
||||
func (d *DNSClient) RemoveRecord(ctx context.Context, serviceName string) error {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
existing, err := d.listRecords(ctx, serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list DNS records: %w", err)
|
||||
}
|
||||
|
||||
for _, r := range existing {
|
||||
d.logger.Info("removing DNS record", "service", serviceName, "id", r.ID)
|
||||
if err := d.deleteRecord(ctx, r.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSClient) listRecords(ctx context.Context, serviceName string) ([]dnsRecord, error) {
|
||||
url := fmt.Sprintf("%s/v1/zones/%s/records?name=%s&type=A", d.serverURL, d.zone, serviceName)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create list request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+d.token)
|
||||
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list records: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read list response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("list records: mcns returned %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
Records []dnsRecord `json:"records"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
return nil, fmt.Errorf("parse list response: %w", err)
|
||||
}
|
||||
return envelope.Records, nil
|
||||
}
|
||||
|
||||
func (d *DNSClient) createRecord(ctx context.Context, serviceName, nodeAddr string) error {
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"name": serviceName, "type": "A", "value": nodeAddr, "ttl": 300,
|
||||
})
|
||||
|
||||
url := fmt.Sprintf("%s/v1/zones/%s/records", d.serverURL, d.zone)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create record request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+d.token)
|
||||
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create record: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("create record: mcns returned %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSClient) updateRecord(ctx context.Context, recordID int, serviceName, nodeAddr string) error {
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"name": serviceName, "type": "A", "value": nodeAddr, "ttl": 300,
|
||||
})
|
||||
|
||||
url := fmt.Sprintf("%s/v1/zones/%s/records/%d", d.serverURL, d.zone, recordID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create update request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+d.token)
|
||||
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update record: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("update record: mcns returned %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSClient) deleteRecord(ctx context.Context, recordID int) error {
|
||||
url := fmt.Sprintf("%s/v1/zones/%s/records/%d", d.serverURL, d.zone, recordID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create delete request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+d.token)
|
||||
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete record: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("delete record: mcns returned %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHTTPClient(caCertPath string) (*http.Client, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
if caCertPath != "" {
|
||||
caCert, err := os.ReadFile(caCertPath) //nolint:gosec // path from trusted config
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read CA cert %q: %w", caCertPath, err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("parse CA cert %q: no valid certificates found", caCertPath)
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
159
internal/master/master.go
Normal file
159
internal/master/master.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package master
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||
"git.wntrmute.dev/mc/mcp/internal/auth"
|
||||
"git.wntrmute.dev/mc/mcp/internal/config"
|
||||
"git.wntrmute.dev/mc/mcp/internal/masterdb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// Master is the MCP cluster master. It coordinates multi-node deployments,
|
||||
// manages edge routes, and stores cluster state.
|
||||
type Master struct {
|
||||
mcpv1.UnimplementedMcpMasterServiceServer
|
||||
|
||||
Config *config.MasterConfig
|
||||
DB *sql.DB
|
||||
Pool *AgentPool
|
||||
DNS *DNSClient
|
||||
Logger *slog.Logger
|
||||
Version string
|
||||
}
|
||||
|
||||
// Run starts the master: opens the database, bootstraps nodes, sets up the
|
||||
// gRPC server with TLS and auth, and blocks until SIGINT/SIGTERM.
|
||||
func Run(cfg *config.MasterConfig, version string) error {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: parseLogLevel(cfg.Log.Level),
|
||||
}))
|
||||
|
||||
// Open master database.
|
||||
db, err := masterdb.Open(cfg.Database.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open master database: %w", err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
// Bootstrap nodes from config.
|
||||
for _, n := range cfg.Nodes {
|
||||
if err := masterdb.UpsertNode(db, n.Name, n.Address, n.Role, "amd64"); err != nil {
|
||||
return fmt.Errorf("bootstrap node %s: %w", n.Name, err)
|
||||
}
|
||||
logger.Info("bootstrapped node", "name", n.Name, "address", n.Address, "role", n.Role)
|
||||
}
|
||||
|
||||
// Load service token for dialing agents.
|
||||
token, err := LoadServiceToken(cfg.Master.ServiceTokenPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load service token: %w", err)
|
||||
}
|
||||
|
||||
// Create agent connection pool.
|
||||
pool := NewAgentPool(cfg.Master.CACert, token)
|
||||
for _, n := range cfg.Nodes {
|
||||
if addErr := pool.AddNode(n.Name, n.Address); addErr != nil {
|
||||
logger.Warn("failed to connect to agent", "node", n.Name, "err", addErr)
|
||||
// Non-fatal: the node may come up later.
|
||||
}
|
||||
}
|
||||
|
||||
// Create DNS client.
|
||||
dns, err := NewDNSClient(cfg.MCNS, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create DNS client: %w", err)
|
||||
}
|
||||
|
||||
m := &Master{
|
||||
Config: cfg,
|
||||
DB: db,
|
||||
Pool: pool,
|
||||
DNS: dns,
|
||||
Logger: logger,
|
||||
Version: version,
|
||||
}
|
||||
|
||||
// TLS.
|
||||
tlsCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load TLS cert: %w", err)
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
// Auth interceptor (same as agent — validates MCIAS tokens).
|
||||
validator, err := auth.NewMCIASValidator(cfg.MCIAS.ServerURL, cfg.MCIAS.CACert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create MCIAS validator: %w", err)
|
||||
}
|
||||
|
||||
// gRPC server.
|
||||
server := grpc.NewServer(
|
||||
grpc.Creds(credentials.NewTLS(tlsConfig)),
|
||||
grpc.ChainUnaryInterceptor(
|
||||
auth.AuthInterceptor(validator),
|
||||
),
|
||||
grpc.ChainStreamInterceptor(
|
||||
auth.StreamAuthInterceptor(validator),
|
||||
),
|
||||
)
|
||||
mcpv1.RegisterMcpMasterServiceServer(server, m)
|
||||
|
||||
// Listen.
|
||||
lis, err := net.Listen("tcp", cfg.Server.GRPCAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen %q: %w", cfg.Server.GRPCAddr, err)
|
||||
}
|
||||
|
||||
logger.Info("master starting",
|
||||
"addr", cfg.Server.GRPCAddr,
|
||||
"version", version,
|
||||
"nodes", len(cfg.Nodes),
|
||||
)
|
||||
|
||||
// Signal handling.
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- server.Serve(lis)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Info("shutting down")
|
||||
server.GracefulStop()
|
||||
pool.Close()
|
||||
return nil
|
||||
case err := <-errCh:
|
||||
pool.Close()
|
||||
return fmt.Errorf("serve: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func parseLogLevel(level string) slog.Level {
|
||||
switch level {
|
||||
case "debug":
|
||||
return slog.LevelDebug
|
||||
case "warn":
|
||||
return slog.LevelWarn
|
||||
case "error":
|
||||
return slog.LevelError
|
||||
default:
|
||||
return slog.LevelInfo
|
||||
}
|
||||
}
|
||||
64
internal/master/placement.go
Normal file
64
internal/master/placement.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package master
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"git.wntrmute.dev/mc/mcp/internal/masterdb"
|
||||
)
|
||||
|
||||
// PickNode selects the best worker node for a new service deployment.
|
||||
// Algorithm: fewest placed services, ties broken alphabetically.
|
||||
func PickNode(db *sql.DB) (string, error) {
|
||||
workers, err := masterdb.ListWorkerNodes(db)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("list workers: %w", err)
|
||||
}
|
||||
if len(workers) == 0 {
|
||||
return "", fmt.Errorf("no worker nodes available")
|
||||
}
|
||||
|
||||
counts, err := masterdb.CountPlacementsPerNode(db)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("count placements: %w", err)
|
||||
}
|
||||
|
||||
// Sort: fewest placements first, then alphabetically.
|
||||
sort.Slice(workers, func(i, j int) bool {
|
||||
ci := counts[workers[i].Name]
|
||||
cj := counts[workers[j].Name]
|
||||
if ci != cj {
|
||||
return ci < cj
|
||||
}
|
||||
return workers[i].Name < workers[j].Name
|
||||
})
|
||||
|
||||
return workers[0].Name, nil
|
||||
}
|
||||
|
||||
// FindMasterNode returns the name of the node with role "master".
|
||||
func FindMasterNode(db *sql.DB) (string, error) {
|
||||
nodes, err := masterdb.ListNodes(db)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("list nodes: %w", err)
|
||||
}
|
||||
for _, n := range nodes {
|
||||
if n.Role == "master" {
|
||||
return n.Name, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no master node found")
|
||||
}
|
||||
|
||||
// FindEdgeNode returns the name of the first edge node.
|
||||
func FindEdgeNode(db *sql.DB) (string, error) {
|
||||
edges, err := masterdb.ListEdgeNodes(db)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("list edge nodes: %w", err)
|
||||
}
|
||||
if len(edges) == 0 {
|
||||
return "", fmt.Errorf("no edge nodes available")
|
||||
}
|
||||
return edges[0].Name, nil
|
||||
}
|
||||
130
internal/master/status.go
Normal file
130
internal/master/status.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package master
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||
"git.wntrmute.dev/mc/mcp/internal/masterdb"
|
||||
)
|
||||
|
||||
// Status returns the status of services across the fleet.
|
||||
func (m *Master) Status(ctx context.Context, req *mcpv1.MasterStatusRequest) (*mcpv1.MasterStatusResponse, error) {
|
||||
m.Logger.Debug("Status", "service", req.GetServiceName())
|
||||
|
||||
resp := &mcpv1.MasterStatusResponse{}
|
||||
|
||||
// If a specific service is requested, look up its placement.
|
||||
if name := req.GetServiceName(); name != "" {
|
||||
placement, err := masterdb.GetPlacement(m.DB, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lookup placement: %w", err)
|
||||
}
|
||||
if placement == nil {
|
||||
return resp, nil // empty — service not found
|
||||
}
|
||||
|
||||
ss := m.getServiceStatus(ctx, placement)
|
||||
resp.Services = append(resp.Services, ss)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// All services.
|
||||
placements, err := masterdb.ListPlacements(m.DB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list placements: %w", err)
|
||||
}
|
||||
|
||||
for _, p := range placements {
|
||||
ss := m.getServiceStatus(ctx, p)
|
||||
resp.Services = append(resp.Services, ss)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *Master) getServiceStatus(ctx context.Context, p *masterdb.Placement) *mcpv1.ServiceStatus {
|
||||
ss := &mcpv1.ServiceStatus{
|
||||
Name: p.ServiceName,
|
||||
Node: p.Node,
|
||||
Tier: p.Tier,
|
||||
Status: "unknown",
|
||||
}
|
||||
|
||||
// Query the agent for live status.
|
||||
client, err := m.Pool.Get(p.Node)
|
||||
if err != nil {
|
||||
ss.Status = "unreachable"
|
||||
return ss
|
||||
}
|
||||
|
||||
statusCtx, cancel := context.WithTimeout(ctx, m.Config.Timeouts.HealthCheck.Duration)
|
||||
defer cancel()
|
||||
|
||||
agentResp, err := client.GetServiceStatus(statusCtx, &mcpv1.GetServiceStatusRequest{
|
||||
Name: p.ServiceName,
|
||||
})
|
||||
if err != nil {
|
||||
ss.Status = "unreachable"
|
||||
return ss
|
||||
}
|
||||
|
||||
// Map agent status to master status.
|
||||
for _, info := range agentResp.GetServices() {
|
||||
if info.GetName() == p.ServiceName {
|
||||
if info.GetActive() {
|
||||
ss.Status = "running"
|
||||
} else {
|
||||
ss.Status = "stopped"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Attach edge route info.
|
||||
edgeRoutes, err := masterdb.ListEdgeRoutesForService(m.DB, p.ServiceName)
|
||||
if err == nil {
|
||||
for _, er := range edgeRoutes {
|
||||
ss.EdgeRoutes = append(ss.EdgeRoutes, &mcpv1.EdgeRouteStatus{
|
||||
Hostname: er.Hostname,
|
||||
EdgeNode: er.EdgeNode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// ListNodes returns all nodes in the registry with placement counts.
|
||||
func (m *Master) ListNodes(_ context.Context, _ *mcpv1.ListNodesRequest) (*mcpv1.ListNodesResponse, error) {
|
||||
m.Logger.Debug("ListNodes")
|
||||
|
||||
nodes, err := masterdb.ListNodes(m.DB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list nodes: %w", err)
|
||||
}
|
||||
|
||||
counts, err := masterdb.CountPlacementsPerNode(m.DB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count placements: %w", err)
|
||||
}
|
||||
|
||||
resp := &mcpv1.ListNodesResponse{}
|
||||
for _, n := range nodes {
|
||||
ni := &mcpv1.NodeInfo{
|
||||
Name: n.Name,
|
||||
Role: n.Role,
|
||||
Address: n.Address,
|
||||
Arch: n.Arch,
|
||||
Status: n.Status,
|
||||
Containers: int32(n.Containers), //nolint:gosec // small number
|
||||
Services: int32(counts[n.Name]), //nolint:gosec // small number
|
||||
}
|
||||
if n.LastHeartbeat != nil {
|
||||
ni.LastHeartbeat = n.LastHeartbeat.Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
resp.Nodes = append(resp.Nodes, ni)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
94
internal/master/undeploy.go
Normal file
94
internal/master/undeploy.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package master
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
||||
"git.wntrmute.dev/mc/mcp/internal/masterdb"
|
||||
)
|
||||
|
||||
// Undeploy handles MasterUndeployRequest: removes edge routes, DNS, then
|
||||
// forwards the undeploy to the worker agent.
|
||||
func (m *Master) Undeploy(ctx context.Context, req *mcpv1.MasterUndeployRequest) (*mcpv1.MasterUndeployResponse, error) {
|
||||
serviceName := req.GetServiceName()
|
||||
if serviceName == "" {
|
||||
return nil, fmt.Errorf("service_name is required")
|
||||
}
|
||||
|
||||
m.Logger.Info("Undeploy", "service", serviceName)
|
||||
|
||||
// Look up placement.
|
||||
placement, err := masterdb.GetPlacement(m.DB, serviceName)
|
||||
if err != nil {
|
||||
return &mcpv1.MasterUndeployResponse{Error: fmt.Sprintf("lookup placement: %v", err)}, nil
|
||||
}
|
||||
if placement == nil {
|
||||
return &mcpv1.MasterUndeployResponse{Error: fmt.Sprintf("service %q not found in placements", serviceName)}, nil
|
||||
}
|
||||
|
||||
// Step 1: Undeploy on worker first (stops the backend).
|
||||
client, err := m.Pool.Get(placement.Node)
|
||||
if err != nil {
|
||||
return &mcpv1.MasterUndeployResponse{Error: fmt.Sprintf("agent connection: %v", err)}, nil
|
||||
}
|
||||
|
||||
undeployCtx, undeployCancel := context.WithTimeout(ctx, m.Config.Timeouts.Undeploy.Duration)
|
||||
defer undeployCancel()
|
||||
|
||||
_, undeployErr := client.UndeployService(undeployCtx, &mcpv1.UndeployServiceRequest{
|
||||
Name: serviceName,
|
||||
})
|
||||
if undeployErr != nil {
|
||||
m.Logger.Warn("agent undeploy failed", "service", serviceName, "node", placement.Node, "err", undeployErr)
|
||||
// Continue — still clean up edge routes and records.
|
||||
}
|
||||
|
||||
// Step 2: Remove edge routes.
|
||||
edgeRoutes, err := masterdb.ListEdgeRoutesForService(m.DB, serviceName)
|
||||
if err != nil {
|
||||
m.Logger.Warn("list edge routes for undeploy", "service", serviceName, "err", err)
|
||||
}
|
||||
for _, er := range edgeRoutes {
|
||||
edgeClient, getErr := m.Pool.Get(er.EdgeNode)
|
||||
if getErr != nil {
|
||||
m.Logger.Warn("edge agent connection", "edge_node", er.EdgeNode, "err", getErr)
|
||||
continue
|
||||
}
|
||||
|
||||
edgeCtx, edgeCancel := context.WithTimeout(ctx, m.Config.Timeouts.EdgeRoute.Duration)
|
||||
_, removeErr := edgeClient.RemoveEdgeRoute(edgeCtx, &mcpv1.RemoveEdgeRouteRequest{
|
||||
Hostname: er.Hostname,
|
||||
})
|
||||
edgeCancel()
|
||||
|
||||
if removeErr != nil {
|
||||
m.Logger.Warn("remove edge route", "hostname", er.Hostname, "err", removeErr)
|
||||
} else {
|
||||
m.Logger.Info("edge route removed", "hostname", er.Hostname, "edge_node", er.EdgeNode)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Remove DNS.
|
||||
if m.DNS != nil {
|
||||
if dnsErr := m.DNS.RemoveRecord(ctx, serviceName); dnsErr != nil {
|
||||
m.Logger.Warn("DNS removal failed", "service", serviceName, "err", dnsErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Clean up records.
|
||||
_ = masterdb.DeleteEdgeRoutesForService(m.DB, serviceName)
|
||||
_ = masterdb.DeletePlacement(m.DB, serviceName)
|
||||
|
||||
success := undeployErr == nil
|
||||
var errMsg string
|
||||
if !success {
|
||||
errMsg = fmt.Sprintf("agent undeploy: %v", undeployErr)
|
||||
}
|
||||
|
||||
m.Logger.Info("undeploy complete", "service", serviceName, "success", success)
|
||||
return &mcpv1.MasterUndeployResponse{
|
||||
Success: success,
|
||||
Error: errMsg,
|
||||
}, nil
|
||||
}
|
||||
106
internal/masterdb/db.go
Normal file
106
internal/masterdb/db.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Package masterdb provides the SQLite database for the mcp-master daemon.
|
||||
// It stores the cluster-wide node registry, service placements, and edge routes.
|
||||
// This is separate from the agent's registry (internal/registry/) because the
|
||||
// master and agent have fundamentally different schemas.
|
||||
package masterdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Open opens the master database at the given path and runs migrations.
|
||||
func Open(path string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
for _, pragma := range []string{
|
||||
"PRAGMA journal_mode = WAL",
|
||||
"PRAGMA foreign_keys = ON",
|
||||
"PRAGMA busy_timeout = 5000",
|
||||
} {
|
||||
if _, err := db.Exec(pragma); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("exec %q: %w", pragma, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := migrate(db); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrate(db *sql.DB) error {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create migrations table: %w", err)
|
||||
}
|
||||
|
||||
for i, m := range migrations {
|
||||
version := i + 1
|
||||
var count int
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = ?", version).Scan(&count); err != nil {
|
||||
return fmt.Errorf("check migration %d: %w", version, err)
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := db.Exec(m); err != nil {
|
||||
return fmt.Errorf("run migration %d: %w", version, err)
|
||||
}
|
||||
if _, err := db.Exec("INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil {
|
||||
return fmt.Errorf("record migration %d: %w", version, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var migrations = []string{
|
||||
// Migration 1: cluster state
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
name TEXT PRIMARY KEY,
|
||||
address TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'worker',
|
||||
arch TEXT NOT NULL DEFAULT 'amd64',
|
||||
status TEXT NOT NULL DEFAULT 'unknown',
|
||||
containers INTEGER NOT NULL DEFAULT 0,
|
||||
last_heartbeat TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS placements (
|
||||
service_name TEXT PRIMARY KEY,
|
||||
node TEXT NOT NULL REFERENCES nodes(name),
|
||||
tier TEXT NOT NULL DEFAULT 'worker',
|
||||
deployed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS edge_routes (
|
||||
hostname TEXT PRIMARY KEY,
|
||||
service_name TEXT NOT NULL,
|
||||
edge_node TEXT NOT NULL REFERENCES nodes(name),
|
||||
backend_hostname TEXT NOT NULL,
|
||||
backend_port INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_edge_routes_service
|
||||
ON edge_routes(service_name);
|
||||
`,
|
||||
}
|
||||
185
internal/masterdb/db_test.go
Normal file
185
internal/masterdb/db_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package masterdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func openTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "test.db")
|
||||
db, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
return db
|
||||
}
|
||||
|
||||
func TestOpenAndMigrate(t *testing.T) {
|
||||
openTestDB(t)
|
||||
}
|
||||
|
||||
func TestNodeCRUD(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
|
||||
if err := UpsertNode(db, "rift", "100.95.252.120:9444", "master", "amd64"); err != nil {
|
||||
t.Fatalf("UpsertNode: %v", err)
|
||||
}
|
||||
if err := UpsertNode(db, "svc", "100.106.232.4:9555", "edge", "amd64"); err != nil {
|
||||
t.Fatalf("UpsertNode: %v", err)
|
||||
}
|
||||
if err := UpsertNode(db, "orion", "100.1.2.3:9444", "worker", "amd64"); err != nil {
|
||||
t.Fatalf("UpsertNode: %v", err)
|
||||
}
|
||||
|
||||
// Get.
|
||||
n, err := GetNode(db, "rift")
|
||||
if err != nil {
|
||||
t.Fatalf("GetNode: %v", err)
|
||||
}
|
||||
if n == nil || n.Address != "100.95.252.120:9444" {
|
||||
t.Errorf("GetNode(rift) = %+v", n)
|
||||
}
|
||||
|
||||
// Get nonexistent.
|
||||
n, err = GetNode(db, "nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("GetNode: %v", err)
|
||||
}
|
||||
if n != nil {
|
||||
t.Errorf("expected nil for nonexistent node")
|
||||
}
|
||||
|
||||
// List all.
|
||||
nodes, err := ListNodes(db)
|
||||
if err != nil {
|
||||
t.Fatalf("ListNodes: %v", err)
|
||||
}
|
||||
if len(nodes) != 3 {
|
||||
t.Errorf("ListNodes: got %d, want 3", len(nodes))
|
||||
}
|
||||
|
||||
// List workers (includes master role).
|
||||
workers, err := ListWorkerNodes(db)
|
||||
if err != nil {
|
||||
t.Fatalf("ListWorkerNodes: %v", err)
|
||||
}
|
||||
if len(workers) != 2 {
|
||||
t.Errorf("ListWorkerNodes: got %d, want 2 (rift+orion)", len(workers))
|
||||
}
|
||||
|
||||
// List edge.
|
||||
edges, err := ListEdgeNodes(db)
|
||||
if err != nil {
|
||||
t.Fatalf("ListEdgeNodes: %v", err)
|
||||
}
|
||||
if len(edges) != 1 || edges[0].Name != "svc" {
|
||||
t.Errorf("ListEdgeNodes: got %v", edges)
|
||||
}
|
||||
|
||||
// Update status.
|
||||
if err := UpdateNodeStatus(db, "rift", "healthy"); err != nil {
|
||||
t.Fatalf("UpdateNodeStatus: %v", err)
|
||||
}
|
||||
n, _ = GetNode(db, "rift")
|
||||
if n.Status != "healthy" {
|
||||
t.Errorf("status = %q, want healthy", n.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlacementCRUD(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
_ = UpsertNode(db, "rift", "100.95.252.120:9444", "master", "amd64")
|
||||
_ = UpsertNode(db, "orion", "100.1.2.3:9444", "worker", "amd64")
|
||||
|
||||
if err := CreatePlacement(db, "mcq", "rift", "worker"); err != nil {
|
||||
t.Fatalf("CreatePlacement: %v", err)
|
||||
}
|
||||
if err := CreatePlacement(db, "mcdoc", "orion", "worker"); err != nil {
|
||||
t.Fatalf("CreatePlacement: %v", err)
|
||||
}
|
||||
|
||||
p, err := GetPlacement(db, "mcq")
|
||||
if err != nil {
|
||||
t.Fatalf("GetPlacement: %v", err)
|
||||
}
|
||||
if p == nil || p.Node != "rift" {
|
||||
t.Errorf("GetPlacement(mcq) = %+v", p)
|
||||
}
|
||||
|
||||
p, _ = GetPlacement(db, "nonexistent")
|
||||
if p != nil {
|
||||
t.Errorf("expected nil for nonexistent placement")
|
||||
}
|
||||
|
||||
counts, err := CountPlacementsPerNode(db)
|
||||
if err != nil {
|
||||
t.Fatalf("CountPlacementsPerNode: %v", err)
|
||||
}
|
||||
if counts["rift"] != 1 || counts["orion"] != 1 {
|
||||
t.Errorf("counts = %v", counts)
|
||||
}
|
||||
|
||||
placements, err := ListPlacements(db)
|
||||
if err != nil {
|
||||
t.Fatalf("ListPlacements: %v", err)
|
||||
}
|
||||
if len(placements) != 2 {
|
||||
t.Errorf("ListPlacements: got %d", len(placements))
|
||||
}
|
||||
|
||||
if err := DeletePlacement(db, "mcq"); err != nil {
|
||||
t.Fatalf("DeletePlacement: %v", err)
|
||||
}
|
||||
p, _ = GetPlacement(db, "mcq")
|
||||
if p != nil {
|
||||
t.Errorf("expected nil after delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdgeRouteCRUD(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
_ = UpsertNode(db, "svc", "100.106.232.4:9555", "edge", "amd64")
|
||||
|
||||
if err := CreateEdgeRoute(db, "mcq.metacircular.net", "mcq", "svc", "mcq.svc.mcp.metacircular.net", 8443); err != nil {
|
||||
t.Fatalf("CreateEdgeRoute: %v", err)
|
||||
}
|
||||
if err := CreateEdgeRoute(db, "docs.metacircular.net", "mcdoc", "svc", "mcdoc.svc.mcp.metacircular.net", 443); err != nil {
|
||||
t.Fatalf("CreateEdgeRoute: %v", err)
|
||||
}
|
||||
|
||||
routes, err := ListEdgeRoutes(db)
|
||||
if err != nil {
|
||||
t.Fatalf("ListEdgeRoutes: %v", err)
|
||||
}
|
||||
if len(routes) != 2 {
|
||||
t.Errorf("ListEdgeRoutes: got %d", len(routes))
|
||||
}
|
||||
|
||||
routes, err = ListEdgeRoutesForService(db, "mcq")
|
||||
if err != nil {
|
||||
t.Fatalf("ListEdgeRoutesForService: %v", err)
|
||||
}
|
||||
if len(routes) != 1 || routes[0].Hostname != "mcq.metacircular.net" {
|
||||
t.Errorf("ListEdgeRoutesForService(mcq) = %v", routes)
|
||||
}
|
||||
|
||||
if err := DeleteEdgeRoute(db, "mcq.metacircular.net"); err != nil {
|
||||
t.Fatalf("DeleteEdgeRoute: %v", err)
|
||||
}
|
||||
routes, _ = ListEdgeRoutes(db)
|
||||
if len(routes) != 1 {
|
||||
t.Errorf("expected 1 route after delete, got %d", len(routes))
|
||||
}
|
||||
|
||||
_ = CreateEdgeRoute(db, "docs2.metacircular.net", "mcdoc", "svc", "mcdoc.svc.mcp.metacircular.net", 443)
|
||||
if err := DeleteEdgeRoutesForService(db, "mcdoc"); err != nil {
|
||||
t.Fatalf("DeleteEdgeRoutesForService: %v", err)
|
||||
}
|
||||
routes, _ = ListEdgeRoutes(db)
|
||||
if len(routes) != 0 {
|
||||
t.Errorf("expected 0 routes after service delete, got %d", len(routes))
|
||||
}
|
||||
}
|
||||
95
internal/masterdb/edge_routes.go
Normal file
95
internal/masterdb/edge_routes.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package masterdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EdgeRoute records a public route managed by the master.
|
||||
type EdgeRoute struct {
|
||||
Hostname string
|
||||
ServiceName string
|
||||
EdgeNode string
|
||||
BackendHostname string
|
||||
BackendPort int
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// CreateEdgeRoute inserts or replaces an edge route record.
|
||||
func CreateEdgeRoute(db *sql.DB, hostname, serviceName, edgeNode, backendHostname string, backendPort int) error {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO edge_routes (hostname, service_name, edge_node, backend_hostname, backend_port, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
service_name = excluded.service_name,
|
||||
edge_node = excluded.edge_node,
|
||||
backend_hostname = excluded.backend_hostname,
|
||||
backend_port = excluded.backend_port
|
||||
`, hostname, serviceName, edgeNode, backendHostname, backendPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create edge route %s: %w", hostname, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListEdgeRoutes returns all edge routes.
|
||||
func ListEdgeRoutes(db *sql.DB) ([]*EdgeRoute, error) {
|
||||
return queryEdgeRoutes(db, `SELECT hostname, service_name, edge_node, backend_hostname, backend_port, created_at FROM edge_routes ORDER BY hostname`)
|
||||
}
|
||||
|
||||
// ListEdgeRoutesForService returns edge routes for a specific service.
|
||||
func ListEdgeRoutesForService(db *sql.DB, serviceName string) ([]*EdgeRoute, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT hostname, service_name, edge_node, backend_hostname, backend_port, created_at
|
||||
FROM edge_routes WHERE service_name = ? ORDER BY hostname
|
||||
`, serviceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list edge routes for %s: %w", serviceName, err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
return scanEdgeRoutes(rows)
|
||||
}
|
||||
|
||||
// DeleteEdgeRoute removes a single edge route by hostname.
|
||||
func DeleteEdgeRoute(db *sql.DB, hostname string) error {
|
||||
_, err := db.Exec(`DELETE FROM edge_routes WHERE hostname = ?`, hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete edge route %s: %w", hostname, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEdgeRoutesForService removes all edge routes for a service.
|
||||
func DeleteEdgeRoutesForService(db *sql.DB, serviceName string) error {
|
||||
_, err := db.Exec(`DELETE FROM edge_routes WHERE service_name = ?`, serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete edge routes for %s: %w", serviceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func queryEdgeRoutes(db *sql.DB, query string) ([]*EdgeRoute, error) {
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query edge routes: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
return scanEdgeRoutes(rows)
|
||||
}
|
||||
|
||||
func scanEdgeRoutes(rows *sql.Rows) ([]*EdgeRoute, error) {
|
||||
var routes []*EdgeRoute
|
||||
for rows.Next() {
|
||||
var r EdgeRoute
|
||||
var createdAt string
|
||||
if err := rows.Scan(&r.Hostname, &r.ServiceName, &r.EdgeNode, &r.BackendHostname, &r.BackendPort, &createdAt); err != nil {
|
||||
return nil, fmt.Errorf("scan edge route: %w", err)
|
||||
}
|
||||
r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
routes = append(routes, &r)
|
||||
}
|
||||
return routes, rows.Err()
|
||||
}
|
||||
103
internal/masterdb/nodes.go
Normal file
103
internal/masterdb/nodes.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package masterdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Node represents a registered node in the cluster.
|
||||
type Node struct {
|
||||
Name string
|
||||
Address string
|
||||
Role string
|
||||
Arch string
|
||||
Status string
|
||||
Containers int
|
||||
LastHeartbeat *time.Time
|
||||
}
|
||||
|
||||
// UpsertNode inserts or updates a node in the registry.
|
||||
func UpsertNode(db *sql.DB, name, address, role, arch string) error {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO nodes (name, address, role, arch, updated_at)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
address = excluded.address,
|
||||
role = excluded.role,
|
||||
arch = excluded.arch,
|
||||
updated_at = datetime('now')
|
||||
`, name, address, role, arch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert node %s: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNode returns a single node by name.
|
||||
func GetNode(db *sql.DB, name string) (*Node, error) {
|
||||
var n Node
|
||||
var lastHB sql.NullString
|
||||
err := db.QueryRow(`
|
||||
SELECT name, address, role, arch, status, containers, last_heartbeat
|
||||
FROM nodes WHERE name = ?
|
||||
`, name).Scan(&n.Name, &n.Address, &n.Role, &n.Arch, &n.Status, &n.Containers, &lastHB)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get node %s: %w", name, err)
|
||||
}
|
||||
if lastHB.Valid {
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", lastHB.String)
|
||||
n.LastHeartbeat = &t
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// ListNodes returns all nodes.
|
||||
func ListNodes(db *sql.DB) ([]*Node, error) {
|
||||
return queryNodes(db, `SELECT name, address, role, arch, status, containers, last_heartbeat FROM nodes ORDER BY name`)
|
||||
}
|
||||
|
||||
// ListWorkerNodes returns nodes with role "worker" or "master" (master is also a worker).
|
||||
func ListWorkerNodes(db *sql.DB) ([]*Node, error) {
|
||||
return queryNodes(db, `SELECT name, address, role, arch, status, containers, last_heartbeat FROM nodes WHERE role IN ('worker', 'master') ORDER BY name`)
|
||||
}
|
||||
|
||||
// ListEdgeNodes returns nodes with role "edge".
|
||||
func ListEdgeNodes(db *sql.DB) ([]*Node, error) {
|
||||
return queryNodes(db, `SELECT name, address, role, arch, status, containers, last_heartbeat FROM nodes WHERE role = 'edge' ORDER BY name`)
|
||||
}
|
||||
|
||||
func queryNodes(db *sql.DB, query string) ([]*Node, error) {
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query nodes: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var nodes []*Node
|
||||
for rows.Next() {
|
||||
var n Node
|
||||
var lastHB sql.NullString
|
||||
if err := rows.Scan(&n.Name, &n.Address, &n.Role, &n.Arch, &n.Status, &n.Containers, &lastHB); err != nil {
|
||||
return nil, fmt.Errorf("scan node: %w", err)
|
||||
}
|
||||
if lastHB.Valid {
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", lastHB.String)
|
||||
n.LastHeartbeat = &t
|
||||
}
|
||||
nodes = append(nodes, &n)
|
||||
}
|
||||
return nodes, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateNodeStatus updates a node's status field.
|
||||
func UpdateNodeStatus(db *sql.DB, name, status string) error {
|
||||
_, err := db.Exec(`UPDATE nodes SET status = ?, updated_at = datetime('now') WHERE name = ?`, status, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update node status %s: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
99
internal/masterdb/placements.go
Normal file
99
internal/masterdb/placements.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package masterdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Placement records which node hosts which service.
|
||||
type Placement struct {
|
||||
ServiceName string
|
||||
Node string
|
||||
Tier string
|
||||
DeployedAt time.Time
|
||||
}
|
||||
|
||||
// CreatePlacement inserts or replaces a placement record.
|
||||
func CreatePlacement(db *sql.DB, serviceName, node, tier string) error {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO placements (service_name, node, tier, deployed_at)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
ON CONFLICT(service_name) DO UPDATE SET
|
||||
node = excluded.node,
|
||||
tier = excluded.tier,
|
||||
deployed_at = datetime('now')
|
||||
`, serviceName, node, tier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create placement %s: %w", serviceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlacement returns the placement for a service.
|
||||
func GetPlacement(db *sql.DB, serviceName string) (*Placement, error) {
|
||||
var p Placement
|
||||
var deployedAt string
|
||||
err := db.QueryRow(`
|
||||
SELECT service_name, node, tier, deployed_at
|
||||
FROM placements WHERE service_name = ?
|
||||
`, serviceName).Scan(&p.ServiceName, &p.Node, &p.Tier, &deployedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get placement %s: %w", serviceName, err)
|
||||
}
|
||||
p.DeployedAt, _ = time.Parse("2006-01-02 15:04:05", deployedAt)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// ListPlacements returns all placements.
|
||||
func ListPlacements(db *sql.DB) ([]*Placement, error) {
|
||||
rows, err := db.Query(`SELECT service_name, node, tier, deployed_at FROM placements ORDER BY service_name`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list placements: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var placements []*Placement
|
||||
for rows.Next() {
|
||||
var p Placement
|
||||
var deployedAt string
|
||||
if err := rows.Scan(&p.ServiceName, &p.Node, &p.Tier, &deployedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan placement: %w", err)
|
||||
}
|
||||
p.DeployedAt, _ = time.Parse("2006-01-02 15:04:05", deployedAt)
|
||||
placements = append(placements, &p)
|
||||
}
|
||||
return placements, rows.Err()
|
||||
}
|
||||
|
||||
// DeletePlacement removes a placement record.
|
||||
func DeletePlacement(db *sql.DB, serviceName string) error {
|
||||
_, err := db.Exec(`DELETE FROM placements WHERE service_name = ?`, serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete placement %s: %w", serviceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountPlacementsPerNode returns a map of node name → number of placed services.
|
||||
func CountPlacementsPerNode(db *sql.DB) (map[string]int, error) {
|
||||
rows, err := db.Query(`SELECT node, COUNT(*) FROM placements GROUP BY node`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count placements: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
counts := make(map[string]int)
|
||||
for rows.Next() {
|
||||
var node string
|
||||
var count int
|
||||
if err := rows.Scan(&node, &count); err != nil {
|
||||
return nil, fmt.Errorf("scan count: %w", err)
|
||||
}
|
||||
counts[node] = count
|
||||
}
|
||||
return counts, rows.Err()
|
||||
}
|
||||
@@ -142,4 +142,18 @@ var migrations = []string{
|
||||
FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE
|
||||
);
|
||||
`,
|
||||
|
||||
// Migration 3: service comment
|
||||
`ALTER TABLE services ADD COLUMN comment TEXT NOT NULL DEFAULT '';`,
|
||||
|
||||
// Migration 4: edge routes (v2 — public routes managed by the master)
|
||||
`CREATE TABLE IF NOT EXISTS edge_routes (
|
||||
hostname TEXT NOT NULL PRIMARY KEY,
|
||||
backend_hostname TEXT NOT NULL,
|
||||
backend_port INTEGER NOT NULL,
|
||||
tls_cert TEXT NOT NULL DEFAULT '',
|
||||
tls_key TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);`,
|
||||
}
|
||||
|
||||
93
internal/registry/edge_routes.go
Normal file
93
internal/registry/edge_routes.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EdgeRoute represents a public edge route managed by the master.
|
||||
type EdgeRoute struct {
|
||||
Hostname string
|
||||
BackendHostname string
|
||||
BackendPort int
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// CreateEdgeRoute inserts or replaces an edge route.
|
||||
func CreateEdgeRoute(db *sql.DB, hostname, backendHostname string, backendPort int, tlsCert, tlsKey string) error {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO edge_routes (hostname, backend_hostname, backend_port, tls_cert, tls_key, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
backend_hostname = excluded.backend_hostname,
|
||||
backend_port = excluded.backend_port,
|
||||
tls_cert = excluded.tls_cert,
|
||||
tls_key = excluded.tls_key,
|
||||
updated_at = datetime('now')
|
||||
`, hostname, backendHostname, backendPort, tlsCert, tlsKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create edge route %s: %w", hostname, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEdgeRoute returns a single edge route by hostname.
|
||||
func GetEdgeRoute(db *sql.DB, hostname string) (*EdgeRoute, error) {
|
||||
var r EdgeRoute
|
||||
var createdAt, updatedAt string
|
||||
err := db.QueryRow(`
|
||||
SELECT hostname, backend_hostname, backend_port, tls_cert, tls_key, created_at, updated_at
|
||||
FROM edge_routes WHERE hostname = ?
|
||||
`, hostname).Scan(&r.Hostname, &r.BackendHostname, &r.BackendPort, &r.TLSCert, &r.TLSKey, &createdAt, &updatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get edge route %s: %w", hostname, err)
|
||||
}
|
||||
r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
r.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// ListEdgeRoutes returns all edge routes.
|
||||
func ListEdgeRoutes(db *sql.DB) ([]*EdgeRoute, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT hostname, backend_hostname, backend_port, tls_cert, tls_key, created_at, updated_at
|
||||
FROM edge_routes ORDER BY hostname
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list edge routes: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var routes []*EdgeRoute
|
||||
for rows.Next() {
|
||||
var r EdgeRoute
|
||||
var createdAt, updatedAt string
|
||||
if err := rows.Scan(&r.Hostname, &r.BackendHostname, &r.BackendPort, &r.TLSCert, &r.TLSKey, &createdAt, &updatedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan edge route: %w", err)
|
||||
}
|
||||
r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
r.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
routes = append(routes, &r)
|
||||
}
|
||||
return routes, rows.Err()
|
||||
}
|
||||
|
||||
// DeleteEdgeRoute removes an edge route by hostname.
|
||||
func DeleteEdgeRoute(db *sql.DB, hostname string) error {
|
||||
result, err := db.Exec(`DELETE FROM edge_routes WHERE hostname = ?`, hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete edge route %s: %w", hostname, err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("edge route %s not found", hostname)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
95
proto/mcp/v1/master.proto
Normal file
95
proto/mcp/v1/master.proto
Normal file
@@ -0,0 +1,95 @@
|
||||
// McpMasterService: Multi-node orchestration for the Metacircular platform.
|
||||
syntax = "proto3";
|
||||
|
||||
package mcp.v1;
|
||||
|
||||
option go_package = "git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1";
|
||||
|
||||
import "proto/mcp/v1/mcp.proto";
|
||||
|
||||
// McpMasterService coordinates multi-node deployments. The CLI sends
|
||||
// deploy/undeploy/status requests to the master, which places services on
|
||||
// nodes, forwards to agents, and coordinates edge routing.
|
||||
service McpMasterService {
|
||||
// CLI operations.
|
||||
rpc Deploy(MasterDeployRequest) returns (MasterDeployResponse);
|
||||
rpc Undeploy(MasterUndeployRequest) returns (MasterUndeployResponse);
|
||||
rpc Status(MasterStatusRequest) returns (MasterStatusResponse);
|
||||
rpc ListNodes(ListNodesRequest) returns (ListNodesResponse);
|
||||
}
|
||||
|
||||
// --- Deploy ---
|
||||
|
||||
message MasterDeployRequest {
|
||||
ServiceSpec service = 1;
|
||||
}
|
||||
|
||||
message MasterDeployResponse {
|
||||
string node = 1; // node the service was placed on
|
||||
bool success = 2; // true only if ALL steps succeeded
|
||||
string error = 3;
|
||||
// Per-step results for operator visibility.
|
||||
StepResult deploy_result = 4;
|
||||
StepResult edge_route_result = 5;
|
||||
StepResult dns_result = 6;
|
||||
}
|
||||
|
||||
message StepResult {
|
||||
string step = 1;
|
||||
bool success = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
// --- Undeploy ---
|
||||
|
||||
message MasterUndeployRequest {
|
||||
string service_name = 1;
|
||||
}
|
||||
|
||||
message MasterUndeployResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
// --- Status ---
|
||||
|
||||
message MasterStatusRequest {
|
||||
string service_name = 1; // empty = all services
|
||||
}
|
||||
|
||||
message MasterStatusResponse {
|
||||
repeated ServiceStatus services = 1;
|
||||
}
|
||||
|
||||
message ServiceStatus {
|
||||
string name = 1;
|
||||
string node = 2;
|
||||
string tier = 3;
|
||||
string status = 4; // "running", "stopped", "unhealthy", "unknown"
|
||||
repeated EdgeRouteStatus edge_routes = 5;
|
||||
}
|
||||
|
||||
message EdgeRouteStatus {
|
||||
string hostname = 1;
|
||||
string edge_node = 2;
|
||||
string cert_expires = 3; // RFC3339
|
||||
}
|
||||
|
||||
// --- Nodes ---
|
||||
|
||||
message ListNodesRequest {}
|
||||
|
||||
message ListNodesResponse {
|
||||
repeated NodeInfo nodes = 1;
|
||||
}
|
||||
|
||||
message NodeInfo {
|
||||
string name = 1;
|
||||
string role = 2;
|
||||
string address = 3;
|
||||
string arch = 4;
|
||||
string status = 5; // "healthy", "unhealthy", "unknown"
|
||||
int32 containers = 6;
|
||||
string last_heartbeat = 7; // RFC3339
|
||||
int32 services = 8; // placement count
|
||||
}
|
||||
@@ -42,6 +42,14 @@ service McpAgentService {
|
||||
rpc AddProxyRoute(AddProxyRouteRequest) returns (AddProxyRouteResponse);
|
||||
rpc RemoveProxyRoute(RemoveProxyRouteRequest) returns (RemoveProxyRouteResponse);
|
||||
|
||||
// Edge routing (called by master on edge nodes)
|
||||
rpc SetupEdgeRoute(SetupEdgeRouteRequest) returns (SetupEdgeRouteResponse);
|
||||
rpc RemoveEdgeRoute(RemoveEdgeRouteRequest) returns (RemoveEdgeRouteResponse);
|
||||
rpc ListEdgeRoutes(ListEdgeRoutesRequest) returns (ListEdgeRoutesResponse);
|
||||
|
||||
// Health (called by master on missed heartbeats)
|
||||
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
|
||||
// Logs
|
||||
rpc Logs(LogsRequest) returns (stream LogsResponse);
|
||||
}
|
||||
@@ -53,6 +61,7 @@ message RouteSpec {
|
||||
int32 port = 2; // mc-proxy listener port (e.g. 443, 8443, 9443); NOT the container internal port
|
||||
string mode = 3; // "l4" or "l7"
|
||||
string hostname = 4; // optional public hostname override
|
||||
bool public = 5; // triggers edge routing when true
|
||||
}
|
||||
|
||||
message ComponentSpec {
|
||||
@@ -68,10 +77,19 @@ message ComponentSpec {
|
||||
repeated string env = 10;
|
||||
}
|
||||
|
||||
message SnapshotConfig {
|
||||
string method = 1; // "grpc", "cli", "exec: <cmd>", "full", or "" (default)
|
||||
repeated string excludes = 2; // paths relative to /srv/<service>/ to skip
|
||||
}
|
||||
|
||||
message ServiceSpec {
|
||||
string name = 1;
|
||||
bool active = 2;
|
||||
repeated ComponentSpec components = 3;
|
||||
string comment = 4;
|
||||
string tier = 5; // "core" or "worker" (default: "worker")
|
||||
string node = 6; // explicit node pin (overrides tier)
|
||||
SnapshotConfig snapshot = 7; // snapshot method and excludes
|
||||
}
|
||||
|
||||
message DeployRequest {
|
||||
@@ -92,6 +110,7 @@ message ComponentResult {
|
||||
|
||||
message StopServiceRequest {
|
||||
string name = 1;
|
||||
string component = 2;
|
||||
}
|
||||
|
||||
message StopServiceResponse {
|
||||
@@ -100,6 +119,7 @@ message StopServiceResponse {
|
||||
|
||||
message StartServiceRequest {
|
||||
string name = 1;
|
||||
string component = 2;
|
||||
}
|
||||
|
||||
message StartServiceResponse {
|
||||
@@ -108,6 +128,7 @@ message StartServiceResponse {
|
||||
|
||||
message RestartServiceRequest {
|
||||
string name = 1;
|
||||
string component = 2;
|
||||
}
|
||||
|
||||
message RestartServiceResponse {
|
||||
@@ -148,6 +169,7 @@ message ServiceInfo {
|
||||
string name = 1;
|
||||
bool active = 2;
|
||||
repeated ComponentInfo components = 3;
|
||||
string comment = 4;
|
||||
}
|
||||
|
||||
message ComponentInfo {
|
||||
@@ -362,6 +384,8 @@ message AddProxyRouteRequest {
|
||||
string backend = 3;
|
||||
string mode = 4; // "l4" or "l7"
|
||||
bool backend_tls = 5;
|
||||
string tls_cert = 6; // path to TLS cert (required for l7)
|
||||
string tls_key = 7; // path to TLS key (required for l7)
|
||||
}
|
||||
|
||||
message AddProxyRouteResponse {}
|
||||
@@ -372,3 +396,43 @@ message RemoveProxyRouteRequest {
|
||||
}
|
||||
|
||||
message RemoveProxyRouteResponse {}
|
||||
|
||||
// --- Edge routes (v2) ---
|
||||
|
||||
message SetupEdgeRouteRequest {
|
||||
string hostname = 1; // public hostname (e.g. "mcq.metacircular.net")
|
||||
string backend_hostname = 2; // internal .svc.mcp hostname
|
||||
int32 backend_port = 3; // port on worker's mc-proxy
|
||||
bool backend_tls = 4; // MUST be true; agent rejects false
|
||||
}
|
||||
|
||||
message SetupEdgeRouteResponse {}
|
||||
|
||||
message RemoveEdgeRouteRequest {
|
||||
string hostname = 1;
|
||||
}
|
||||
|
||||
message RemoveEdgeRouteResponse {}
|
||||
|
||||
message ListEdgeRoutesRequest {}
|
||||
|
||||
message ListEdgeRoutesResponse {
|
||||
repeated EdgeRoute routes = 1;
|
||||
}
|
||||
|
||||
message EdgeRoute {
|
||||
string hostname = 1;
|
||||
string backend_hostname = 2;
|
||||
int32 backend_port = 3;
|
||||
string cert_serial = 4;
|
||||
string cert_expires = 5; // RFC3339
|
||||
}
|
||||
|
||||
// --- Health check (v2) ---
|
||||
|
||||
message HealthCheckRequest {}
|
||||
|
||||
message HealthCheckResponse {
|
||||
string status = 1; // "healthy" or "degraded"
|
||||
int32 containers = 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user