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 {
|
func deployCmd() *cobra.Command {
|
||||||
|
var direct bool
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "deploy <service>[/<component>]",
|
Use: "deploy <service>[/<component>]",
|
||||||
Short: "Deploy service from service definition",
|
Short: "Deploy service from service definition",
|
||||||
@@ -40,6 +42,12 @@ func deployCmd() *cobra.Command {
|
|||||||
|
|
||||||
spec := servicedef.ToProto(def)
|
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)
|
address, err := findNodeAddress(cfg, def.Node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -64,9 +72,48 @@ func deployCmd() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringP("file", "f", "", "service definition file")
|
cmd.Flags().StringP("file", "f", "", "service definition file")
|
||||||
|
cmd.Flags().BoolVar(&direct, "direct", false, "bypass master, deploy directly to agent (v1 mode)")
|
||||||
return cmd
|
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.
|
// parseServiceArg splits a "service/component" argument into its parts.
|
||||||
func parseServiceArg(arg string) (service, component string) {
|
func parseServiceArg(arg string) (service, component string) {
|
||||||
parts := strings.SplitN(arg, "/", 2)
|
parts := strings.SplitN(arg, "/", 2)
|
||||||
@@ -125,8 +172,9 @@ func loadServiceDef(cmd *cobra.Command, cfg *config.CLIConfig, serviceName strin
|
|||||||
// ServiceInfo, not ServiceSpec.
|
// ServiceInfo, not ServiceSpec.
|
||||||
func serviceSpecFromInfo(info *mcpv1.ServiceInfo) *mcpv1.ServiceSpec {
|
func serviceSpecFromInfo(info *mcpv1.ServiceInfo) *mcpv1.ServiceSpec {
|
||||||
spec := &mcpv1.ServiceSpec{
|
spec := &mcpv1.ServiceSpec{
|
||||||
Name: info.GetName(),
|
Name: info.GetName(),
|
||||||
Active: info.GetActive(),
|
Active: info.GetActive(),
|
||||||
|
Comment: info.GetComment(),
|
||||||
}
|
}
|
||||||
for _, c := range info.GetComponents() {
|
for _, c := range info.GetComponents() {
|
||||||
spec.Components = append(spec.Components, &mcpv1.ComponentSpec{
|
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
|
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
|
// tokenInterceptor returns a gRPC client interceptor that attaches the
|
||||||
// bearer token to outgoing RPC metadata.
|
// bearer token to outgoing RPC metadata.
|
||||||
func tokenInterceptor(token string) grpc.UnaryClientInterceptor {
|
func tokenInterceptor(token string) grpc.UnaryClientInterceptor {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func undeployCmd() *cobra.Command {
|
func undeployCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
var direct bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
Use: "undeploy <service>",
|
Use: "undeploy <service>",
|
||||||
Short: "Fully undeploy a service: remove routes, DNS, certs, and containers",
|
Short: "Fully undeploy a service: remove routes, DNS, certs, and containers",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
@@ -38,6 +40,11 @@ func undeployCmd() *cobra.Command {
|
|||||||
return fmt.Errorf("write service def: %w", err)
|
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)
|
address, err := findNodeAddress(cfg, def.Node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -60,4 +67,28 @@ func undeployCmd() *cobra.Command {
|
|||||||
return nil
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,19 @@ import (
|
|||||||
|
|
||||||
// CLIConfig is the configuration for the mcp CLI binary.
|
// CLIConfig is the configuration for the mcp CLI binary.
|
||||||
type CLIConfig struct {
|
type CLIConfig struct {
|
||||||
Services ServicesConfig `toml:"services"`
|
Services ServicesConfig `toml:"services"`
|
||||||
Build BuildConfig `toml:"build"`
|
Build BuildConfig `toml:"build"`
|
||||||
MCIAS MCIASConfig `toml:"mcias"`
|
MCIAS MCIASConfig `toml:"mcias"`
|
||||||
Auth AuthConfig `toml:"auth"`
|
Auth AuthConfig `toml:"auth"`
|
||||||
Nodes []NodeConfig `toml:"nodes"`
|
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.
|
// BuildConfig holds settings for building container images.
|
||||||
|
|||||||
Reference in New Issue
Block a user