Its own module rather than a package under shared, so cobra and pflag stay out of the module graphs of server, admin and sitesvc, which never use them.
125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
// Package cmd is vantagectl's command tree.
|
|
//
|
|
// It holds argument parsing and operator-facing output only. Everything it does
|
|
// to a database goes through shared/backup, which the server can also import.
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
|
)
|
|
|
|
const connectTimeout = 30 * time.Second
|
|
|
|
// globalOpts is what every subcommand needs.
|
|
type globalOpts struct {
|
|
MongoURI string
|
|
Database string
|
|
KeyHex string
|
|
}
|
|
|
|
// NewRoot builds the command tree.
|
|
func NewRoot(version string) *cobra.Command {
|
|
root := &cobra.Command{
|
|
Use: "vantagectl",
|
|
Short: "Back up and restore a Vantage control plane",
|
|
Version: version,
|
|
Long: "vantagectl backs up and restores the MongoDB database behind a Vantage\n" +
|
|
"control plane.\n\n" +
|
|
"It talks to MongoDB directly and never to the Vantage API, so it works\n" +
|
|
"against a control plane that is down, half-migrated, or gone.\n\n" +
|
|
"KEY_ENCRYPTION_KEY is never written into an archive. What an archive\n" +
|
|
"records is a fingerprint of it, so a restore can tell you that the key\n" +
|
|
"you hold is the wrong one before it writes a database nobody can read.",
|
|
SilenceUsage: true,
|
|
}
|
|
|
|
f := root.PersistentFlags()
|
|
f.String("mongo-uri", "", "MongoDB connection string (env MONGO_URI)")
|
|
f.String("db", "", "database name (env MONGO_DB, or the URI path)")
|
|
|
|
root.AddCommand(newBackupCmd(), newRestoreCmd(), newInspectCmd(), newVerifyCmd())
|
|
return root
|
|
}
|
|
|
|
// Execute runs the tree.
|
|
func Execute(version string) error {
|
|
return NewRoot(version).Execute()
|
|
}
|
|
|
|
// resolveGlobals applies the environment fallback.
|
|
//
|
|
// Explicit flags win. The check is on Changed rather than on emptiness, so
|
|
// `--db ""` is an explicit empty value rather than an invitation to read the
|
|
// environment behind the operator's back.
|
|
func resolveGlobals(c *cobra.Command) (*globalOpts, error) {
|
|
root := c.Root()
|
|
f := root.PersistentFlags()
|
|
|
|
uri, err := f.GetString("mongo-uri")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !f.Changed("mongo-uri") {
|
|
uri = os.Getenv("MONGO_URI")
|
|
}
|
|
if uri == "" {
|
|
return nil, fmt.Errorf("no MongoDB URI: pass --mongo-uri or set MONGO_URI")
|
|
}
|
|
|
|
db, err := f.GetString("db")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !f.Changed("db") {
|
|
db = os.Getenv("MONGO_DB")
|
|
}
|
|
if db == "" {
|
|
db = databaseFromURI(uri)
|
|
}
|
|
if db == "" {
|
|
return nil, fmt.Errorf("no database name: pass --db, set MONGO_DB, or put one in the URI path")
|
|
}
|
|
|
|
return &globalOpts{
|
|
MongoURI: uri,
|
|
Database: db,
|
|
KeyHex: strings.TrimSpace(os.Getenv("KEY_ENCRYPTION_KEY")),
|
|
}, nil
|
|
}
|
|
|
|
// databaseFromURI reads the database out of the URI path. sitesvc takes its
|
|
// database name this way too, so an operator who has configured one has
|
|
// configured both.
|
|
func databaseFromURI(uri string) string {
|
|
u, err := url.Parse(uri)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.Trim(u.Path, "/")
|
|
}
|
|
|
|
// connect dials MongoDB and proves the connection before a caller commits to
|
|
// anything.
|
|
func connect(ctx context.Context, g *globalOpts) (*mongo.Client, error) {
|
|
client, err := mongo.Connect(options.Client().ApplyURI(g.MongoURI))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("connect to MongoDB: %w", err)
|
|
}
|
|
pingCtx, cancel := context.WithTimeout(ctx, connectTimeout)
|
|
defer cancel()
|
|
if err := client.Ping(pingCtx, nil); err != nil {
|
|
_ = client.Disconnect(context.Background())
|
|
return nil, fmt.Errorf("MongoDB did not answer: %w", err)
|
|
}
|
|
return client, nil
|
|
}
|