Add master integration to CLI deploy and undeploy
- CLIConfig gains optional [master] section with address field - dialMaster() creates McpMasterServiceClient (same TLS/token pattern) - deploy: routes through master when [master] configured, --direct flag bypasses master for v1-style agent deployment - undeploy: same master/direct routing pattern - Master responses show per-step results (deploy, dns, edge) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
@@ -127,6 +174,7 @@ func serviceSpecFromInfo(info *mcpv1.ServiceInfo) *mcpv1.ServiceSpec {
|
||||
spec := &mcpv1.ServiceSpec{
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -15,6 +15,14 @@ type CLIConfig struct {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user