Initial implementation of mcdeploy deployment tool

Single Go binary with five commands:
- build: podman build locally with registry tags + git version
- push: podman push to MCR
- deploy: SSH pull/stop/rm/run on target node
- cert renew: issue TLS cert from Metacrypt via REST API
- status: show container status on a node

Config-driven via TOML service registry describing images,
Dockerfiles, container configs per node. Shells out to podman
for container operations and ssh for remote access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 00:01:15 -07:00
commit 8cd32cbb1c
12 changed files with 729 additions and 0 deletions

215
cert.go Normal file
View File

@@ -0,0 +1,215 @@
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/spf13/cobra"
)
func certCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cert",
Short: "Certificate management",
}
cmd.AddCommand(certRenewCommand())
return cmd
}
func certRenewCommand() *cobra.Command {
var installFlag string
var caCertFlag string
cmd := &cobra.Command{
Use: "renew <hostname>",
Short: "Request a new certificate from Metacrypt",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
hostname := args[0]
addr := os.Getenv("METACRYPT_ADDR")
if addr == "" {
addr = "https://metacrypt.svc.mcp.metacircular.net:8443"
}
token := os.Getenv("METACRYPT_TOKEN")
if token == "" {
return fmt.Errorf("METACRYPT_TOKEN environment variable is required")
}
// Build request body.
reqBody := map[string]any{
"mount": "pki",
"operation": "issue",
"path": "web",
"data": map[string]any{
"issuer": "web",
"common_name": hostname,
"profile": "server",
"dns_names": []string{hostname},
"ttl": "2160h",
},
}
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
// Configure TLS.
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS13,
}
if caCertFlag != "" {
caPEM, err := os.ReadFile(caCertFlag)
if err != nil {
return fmt.Errorf("read CA cert: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caPEM) {
return fmt.Errorf("failed to parse CA cert")
}
tlsCfg.RootCAs = pool
} else {
tlsCfg.InsecureSkipVerify = true
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsCfg,
},
}
url := addr + "/v1/engine/request"
fmt.Printf(">>> POST %s\n", url)
req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("metacrypt returned %d: %s", resp.StatusCode, string(respBody))
}
var result map[string]any
if err := json.Unmarshal(respBody, &result); err != nil {
return fmt.Errorf("parse response: %w", err)
}
certPEM, _ := result["cert_pem"].(string)
chainPEM, _ := result["chain_pem"].(string)
keyPEM, _ := result["key_pem"].(string)
if certPEM == "" || keyPEM == "" {
return fmt.Errorf("response missing cert_pem or key_pem")
}
// Combine leaf and chain into full cert.
fullCert := certPEM
if chainPEM != "" {
fullCert = certPEM + "\n" + chainPEM
}
// Parse and display cert details.
tlsCert, err := tls.X509KeyPair([]byte(fullCert), []byte(keyPEM))
if err != nil {
return fmt.Errorf("parse cert: %w", err)
}
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
return fmt.Errorf("parse leaf cert: %w", err)
}
fmt.Printf("Certificate issued:\n")
fmt.Printf(" Serial: %s\n", leaf.SerialNumber.Text(16))
fmt.Printf(" CN: %s\n", leaf.Subject.CommonName)
fmt.Printf(" Expires: %s\n", leaf.NotAfter.Format("2006-01-02 15:04:05 UTC"))
// Install to remote node if requested.
if installFlag != "" {
parts := strings.SplitN(installFlag, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("--install must be <node>:<path> (e.g. mcp:/srv/mc-proxy/tls)")
}
node := parts[0]
remotePath := parts[1]
cfg, err := loadCfg()
if err != nil {
return err
}
nodeCfg, err := cfg.FindNode(node)
if err != nil {
return err
}
host := nodeCfg.Host
if nodeCfg.User != "" {
host = nodeCfg.User + "@" + nodeCfg.Host
}
// Write temp files and SCP them.
certFile, err := os.CreateTemp("", "cert-*.pem")
if err != nil {
return fmt.Errorf("create temp cert: %w", err)
}
defer os.Remove(certFile.Name())
keyFile, err := os.CreateTemp("", "key-*.pem")
if err != nil {
return fmt.Errorf("create temp key: %w", err)
}
defer os.Remove(keyFile.Name())
if _, err := certFile.WriteString(fullCert); err != nil {
return fmt.Errorf("write temp cert: %w", err)
}
certFile.Close()
if _, err := keyFile.WriteString(keyPEM); err != nil {
return fmt.Errorf("write temp key: %w", err)
}
keyFile.Close()
certDest := host + ":" + remotePath + "/cert.pem"
keyDest := host + ":" + remotePath + "/key.pem"
if err := run("scp", certFile.Name(), certDest); err != nil {
return fmt.Errorf("scp cert: %w", err)
}
if err := run("scp", keyFile.Name(), keyDest); err != nil {
return fmt.Errorf("scp key: %w", err)
}
fmt.Printf("Installed cert and key to %s:%s/\n", node, remotePath)
}
return nil
},
}
cmd.Flags().StringVar(&installFlag, "install", "", "install cert to <node>:<path>")
cmd.Flags().StringVar(&caCertFlag, "ca-cert", "", "CA certificate for Metacrypt TLS verification")
return cmd
}