Repository: kolide/launcher

Issue:
Update `launcher compactdb` to allow for compacting backup dbs


Discussion:
@directionless: One callout here -- I don't entirely trust compactdb. So I don't want to compact _everything_, I'd rather make people be explicti about which files

Solution patch:
diff --git a/cmd/launcher/run_compactdb.go b/cmd/launcher/run_compactdb.go
index 73048648..49b5fd69 100644
--- a/cmd/launcher/run_compactdb.go
+++ b/cmd/launcher/run_compactdb.go
@@ -3,40 +3,78 @@ package main
 import (
 	"context"
 	"errors"
+	"flag"
+	"fmt"
 	"log/slog"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/kolide/launcher/ee/agent"
 	"github.com/kolide/launcher/pkg/launcher"
 	"github.com/kolide/launcher/pkg/log/multislogger"
+	"github.com/peterbourgon/ff/v3"
 )
 
 func runCompactDb(systemMultiSlogger *multislogger.MultiSlogger, args []string) error {
-	opts, err := launcher.ParseOptions("compactdb", args)
-	if err != nil {
-		return err
+	var (
+		flagset = flag.NewFlagSet("launcher compactdb", flag.ExitOnError)
+		// Flags specific to this subcommand
+		flDbFileName     = flagset.String("db", "launcher.db", "The launcher.db or launcher.db.bak file to be compacted")
+		flCompactDbMaxTx = flagset.Int64("compactdb-max-tx", 65536, "Maximum transaction size used when compacting the internal DB")
+		// Flags shared by runLauncher/other subcommands, to be parsed by launcher.ParseOptions
+		flRootDirectory  = flagset.String("root_directory", "", "The location of the local database, pidfiles, etc.")
+		flConfigFilePath = flagset.String("config", "", "config file to parse options from (optional)")
+	)
+	flagset.Usage = commandUsage(flagset, "launcher compactdb")
+	if err := ff.Parse(flagset, args); err != nil {
+		return fmt.Errorf("parsing flags: %w", err)
 	}
 
-	if opts.RootDirectory == "" {
-		return errors.New("no root directory specified")
+	launcherOptions := make([]string, 0)
+	if *flRootDirectory != "" {
+		launcherOptions = append(launcherOptions, "-root_directory", *flRootDirectory)
+	}
+	if *flConfigFilePath != "" {
+		launcherOptions = append(launcherOptions, "-config", *flConfigFilePath)
 	}
 
-	// relevel
-	slogLevel := slog.LevelInfo
-	if opts.Debug {
-		slogLevel = slog.LevelDebug
+	opts, err := launcher.ParseOptions("compactdb", launcherOptions)
+	if err != nil {
+		return fmt.Errorf("parsing launcher options: %w", err)
+	}
+	if opts.RootDirectory == "" {
+		return errors.New("no root directory specified")
 	}
 
 	// Add handler to write to stdout
 	systemMultiSlogger.AddHandler(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
-		Level:     slogLevel,
+		Level:     slog.LevelDebug,
 		AddSource: true,
 	}))
 
 	boltPath := filepath.Join(opts.RootDirectory, "launcher.db")
+	if *flDbFileName != "" {
+		// Check to make sure this is a launcher.db or launcher.db.bak file
+		givenFile := filepath.Base(*flDbFileName)
+		if givenFile != "launcher.db" && !strings.HasPrefix(givenFile, "launcher.db.bak") {
+			return fmt.Errorf("invalid db file given, must be launcher.db or a backup: %s", *flDbFileName)
+		}
+
+		boltPath = filepath.Join(opts.RootDirectory, givenFile)
+
+		// If a directory was provided, make sure it matches the root directory
+		if givenFile != *flDbFileName && boltPath != *flDbFileName {
+			return fmt.Errorf("db file %s is not in root directory (expected %s)", *flDbFileName, boltPath)
+		}
+	}
+
+	systemMultiSlogger.Log(context.TODO(), slog.LevelInfo,
+		"preparing to compact db",
+		"path", boltPath,
+	)
 
-	oldDbPath, err := agent.DbCompact(boltPath, opts.CompactDbMaxTx)
+	oldDbPath, err := agent.DbCompact(boltPath, *flCompactDbMaxTx)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/launcher/options.go b/pkg/launcher/options.go
index d04d6e86..184c9d24 100644
--- a/pkg/launcher/options.go
+++ b/pkg/launcher/options.go
@@ -107,8 +107,6 @@ type Options struct {
 	InsecureTLS bool
 	// InsecureTransport disables TLS in the transport layer.
 	InsecureTransport bool
-	// CompactDbMaxTx sets the max transaction size for bolt db compaction operations
-	CompactDbMaxTx int64
 	// IAmBreakingEELicence disables the EE licence check before running the local server
 	IAmBreakingEELicense bool
 	// DelayStart allows for delaying launcher startup for a configurable amount of time
@@ -219,7 +217,6 @@ func ParseOptions(subcommandName string, args []string) (*Options, error) {
 		flVersion                         = flagset.Bool("version", false, "Print Launcher version and exit")
 		flLogMaxBytesPerBatch             = flagset.Int("log_max_bytes_per_batch", 0, "Maximum size of a batch of logs. Recommend leaving unset, and launcher will determine")
 		flOsqueryFlags                    ArrayFlags // set below with flagset.Var
-		flCompactDbMaxTx                  = flagset.Int64("compactdb-max-tx", 65536, "Maximum transaction size used when compacting the internal DB")
 		flConfigFilePath                  = flagset.String("config", DefaultConfigFilePath, "config file to parse options from (optional)")
 		flExportTraces                    = flagset.Bool("export_traces", false, "Whether to export traces")
 		flTraceSamplingRate               = flagset.Float64("trace_sampling_rate", 0.0, "What fraction of traces should be sampled")
@@ -259,6 +256,7 @@ func ParseOptions(subcommandName string, args []string) (*Options, error) {
 		_ = flagset.String("logger_tls_endpoint", "", "DEPRECATED")
 		_ = flagset.String("distributed_tls_read_endpoint", "", "DEPRECATED")
 		_ = flagset.String("distributed_tls_write_endpoint", "", "DEPRECATED")
+		_ = flagset.Int64("compactdb-max-tx", 65536, "DEPRECATED") // moved to new flagset inside compactdb command
 	)
 
 	flagset.Var(&flOsqueryFlags, "osquery_flag", "Flags to pass to osquery (possibly overriding Launcher defaults)")
@@ -377,7 +375,6 @@ func ParseOptions(subcommandName string, args []string) (*Options, error) {
 		AutoupdateInterval:              *flAutoupdateInterval,
 		AutoupdateInitialDelay:          *flAutoupdateInitialDelay,
 		CertPins:                        certPins,
-		CompactDbMaxTx:                  *flCompactDbMaxTx,
 		ConfigFilePath:                  *flConfigFilePath,
 		Control:                         false,
 		ControlServerURL:                controlServerURL,



Test patch:
diff --git a/pkg/launcher/options_test.go b/pkg/launcher/options_test.go
index cb9f2076..3620d2f8 100644
--- a/pkg/launcher/options_test.go
+++ b/pkg/launcher/options_test.go
@@ -243,7 +243,6 @@ func getArgsAndResponse() (map[string]string, *Options) {
 	opts := &Options{
 		AutoupdateInitialDelay:          1 * time.Hour,
 		AutoupdateInterval:              48 * time.Hour,
-		CompactDbMaxTx:                  int64(65536),
 		Control:                         false,
 		ControlServerURL:                "",
 		ControlRequestInterval:          60 * time.Second,