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:
215
cert.go
Normal file
215
cert.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user