Add status command, deployment infrastructure, and fix proto paths

Rename proto/gen directories from mc-proxy to mc_proxy for valid protobuf
package naming. Add CLI status subcommand for querying running instance
health via gRPC. Add systemd backup service/timer and backup pruning
script. Add buf.yaml and proto-lint Makefile target. Add shutdown_timeout
config field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 08:34:37 -07:00
parent dc04a070a3
commit f1e9834bd3
15 changed files with 297 additions and 120 deletions

View File

@@ -12,6 +12,7 @@ func rootCmd() *cobra.Command {
}
cmd.AddCommand(serverCmd())
cmd.AddCommand(statusCmd())
cmd.AddCommand(snapshotCmd())
return cmd

101
cmd/mc-proxy/status.go Normal file
View File

@@ -0,0 +1,101 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
pb "git.wntrmute.dev/kyle/mc-proxy/gen/mc_proxy/v1"
"git.wntrmute.dev/kyle/mc-proxy/internal/config"
)
func statusCmd() *cobra.Command {
var configPath string
cmd := &cobra.Command{
Use: "status",
Short: "Query a running instance's health and status",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load(configPath)
if err != nil {
return err
}
if cfg.GRPC.Addr == "" {
return fmt.Errorf("gRPC admin API is not configured (grpc.addr is empty)")
}
conn, err := dialGRPC(cfg.GRPC)
if err != nil {
return fmt.Errorf("connecting to gRPC API: %w", err)
}
defer conn.Close()
client := pb.NewProxyAdminServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetStatus(ctx, &pb.GetStatusRequest{})
if err != nil {
return fmt.Errorf("getting status: %w", err)
}
fmt.Printf("mc-proxy %s\n", resp.Version)
if resp.StartedAt != nil {
uptime := time.Since(resp.StartedAt.AsTime()).Truncate(time.Second)
fmt.Printf("uptime: %s\n", uptime)
}
fmt.Printf("connections: %d\n", resp.TotalConnections)
fmt.Println()
for _, ls := range resp.Listeners {
fmt.Printf(" %s routes=%d active=%d\n", ls.Addr, ls.RouteCount, ls.ActiveConnections)
}
return nil
},
}
cmd.Flags().StringVarP(&configPath, "config", "c", "mc-proxy.toml", "path to configuration file")
return cmd
}
func dialGRPC(cfg config.GRPC) (*grpc.ClientConn, error) {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS13,
}
// Load CA cert for verifying the server.
if cfg.CACert != "" {
caCert, err := os.ReadFile(cfg.CACert)
if err != nil {
return nil, fmt.Errorf("reading CA cert: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to parse CA certificate")
}
tlsConfig.RootCAs = pool
}
// Load client cert for mTLS.
if cfg.TLSCert != "" && cfg.TLSKey != "" {
cert, err := tls.LoadX509KeyPair(cfg.TLSCert, cfg.TLSKey)
if err != nil {
return nil, fmt.Errorf("loading client cert: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
creds := credentials.NewTLS(tlsConfig)
return grpc.NewClient(cfg.Addr, grpc.WithTransportCredentials(creds))
}