FDM updates: fdm serve, snapshot/restore improvements (#27890)

For #27889 

This PR introduces several improvements to the Makefile/`fdm` tool for
development:
 
### `fdm serve` (alias `fdm up`)

Starts a local Fleet server (building the binary first). The first time
this is called, it will start the server on `localhost:8080` with the
`--dev` and `--dev_license` flags, but the command accepts all of the
options that you can pass to `fleet serve`. If you pass options to `fdm
serve`, then subsequent invocations _without_ options will replay your
last command. Additionally, `fdm serve` supports the following:

- `--use-ip`: start the local server on your system's local IP address
rather than `localhost`. This makes it easier to point VMs on your
system to the fleet server to act as hosts.
- `--no-build`: don't rebuild the fleet binary before starting the
server.
- `--no-save`: don't save the current command for future invocations
(useful for scripting)
- `--show`: show options for the last-invoked `fdm serve` command
- `--reset`: reset the options for `fdm serve`. The next time `fdm
serve` is invoked, it will use the default options.
- `--help`: show all of the Fleet server options

### `fdm snapshot` improvements

* Added `fdm snap` alias
* Tracks the name of the last snapshot saved, to use as the default for
`fdm restore`
* Suppresses the "don't use password in CLI" warning when saving the
snapshot

### `fdm restore` improvements

* Added `--prep` / `--prepare` option to run db migrations after
restoring snapshot.
* Improved UI (more options displayed, and clearer indicator for
selected option)
* Now defaults to last snapshot restored
This commit is contained in:
Scott Gress 2025-04-07 09:10:15 -05:00 committed by GitHub
parent df61e6b7f5
commit d51f2815ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 141 additions and 12 deletions

102
Makefile
View file

@ -59,6 +59,15 @@ LDFLAGS_VERSION = "\
-X github.com/fleetdm/fleet/v4/server/version.buildUser=${USER} \
-X github.com/fleetdm/fleet/v4/server/version.goVersion=${GOVERSION}"
# Macro to allow targets to filter out their own arguments from the arguments
# passed to the final command.
# Targets may also add their own CLI arguments to the command as EXTRA_CLI_ARGS.
# See `serve` target for an example.
define filter_args
$(eval FORWARDED_ARGS := $(filter-out $(TARGET_ARGS), $(CLI_ARGS)))
$(eval FORWARDED_ARGS := $(FORWARDED_ARGS) $(EXTRA_CLI_ARGS))
endef
all: build
@ -112,6 +121,71 @@ fdm:
sudo ln -sf "$$(pwd)/build/fdm" /usr/local/bin/fdm; \
fi
.help-short--serve:
@echo "Start the fleet server"
.help-short--up:
@echo "Start the fleet server (alias for \`serve\`)"
.help-long--serve: SERVE_CMD:=serve
.help-long--up: SERVE_CMD:=up
.help-long--serve .help-long--up:
@echo "Starts an instance of the Fleet web and API server."
@echo
@echo " By default the server will listen on localhost:8080, in development mode with a premium license."
@echo " If different options are used to start the server, the options will become 'sticky' and will be used the next time \`$(TOOL_CMD) $(SERVE_CMD)\` is called."
@echo
@echo " To see all available options, run \`$(TOOL_CMD) $(SERVE_CMD) --help\`"
.help-options--serve .help-options--up:
@echo "HELP"
@echo "Show all options for the fleet serve command"
@echo "USE_IP"
@echo "Start the server on the IP address of the host machine"
@echo "NO_BUILD"
@echo "Don't build the fleet binary before starting the server"
@echo "NO_SAVE"
@echo "Don't save the current arguments for the next invocation"
@echo "SHOW"
@echo "Show the last arguments used to start the server"
up: SERVE_CMD:=up
up: serve
serve: SERVE_CMD:=serve
serve: TARGET_ARGS := --use-ip --no-save --show --no-build
ifdef USE_IP
serve: EXTRA_CLI_ARGS := $(EXTRA_CLI_ARGS) --server_address=$(shell ipconfig getifaddr en0):8080
endif
ifdef SHOW
serve:
@SAVED_ARGS=$$(cat ~/.fleet/last-serve-invocation); \
if [[ $$? -eq 0 ]]; then \
echo "$$SAVED_ARGS"; \
fi
else ifdef HELP
serve:
@./build/fleet serve --help
else ifdef RESET
serve:
@touch ~/.fleet/last-serve-invocation && rm ~/.fleet/last-serve-invocation
else
serve:
@if [[ "$(NO_BUILD)" != "true" ]]; then make fleet; fi
$(call filter_args)
# If FORWARDED_ARGS is not empty, run the command with the forwarded arguments.
# Unless NO_SAVE is set to true, save the command to the last invocation file.
# IF FORWARDED_ARGS is empty, attempt to repeat the last invocation.
@if [[ "$(FORWARDED_ARGS)" != "" ]]; then \
if [[ "$(NO_SAVE)" != "true" ]]; then \
echo "./build/fleet serve $(FORWARDED_ARGS)" > ~/.fleet/last-serve-invocation; \
fi; \
./build/fleet serve $(FORWARDED_ARGS); \
else \
if ! [[ -f ~/.fleet/last-serve-invocation ]]; then \
echo "./build/fleet serve --server_address=localhost:8080 --dev --dev_license" > ~/.fleet/last-serve-invocation; \
fi; \
cat ~/.fleet/last-serve-invocation; \
$$(cat ~/.fleet/last-serve-invocation); \
fi
endif
fleet: .prefix .pre-build .pre-fleet
CGO_ENABLED=1 go build -race=${GO_BUILD_RACE_ENABLED_VAR} -tags full,fts5,netgo -o build/${OUTPUT} -ldflags ${LDFLAGS_VERSION} ./cmd/fleet
@ -500,13 +574,32 @@ db-restore:
# Interactive snapshot / restore
.help-short--snap .help-short--snapshot:
@echo "Snapshot the database"
.help-long--snap .help-long--snapshot:
@echo "Interactively take a snapshot of the present database state. Restore snapshots with \`$(TOOL_CMD) restore\`."
SNAPSHOT_BINARY = ./build/snapshot
snapshot: $(SNAPSHOT_BINARY)
snap snapshot: $(SNAPSHOT_BINARY)
@ $(SNAPSHOT_BINARY) snapshot
$(SNAPSHOT_BINARY): tools/snapshot/*.go
cd tools/snapshot && go build -o ../../build/snapshot
.help-short--restore:
@echo "Restore a database snapshot"
.help-long--restore:
@echo "Interactively restore database state using a snapshot taken with \`$(TOOL_CMD) snapshot\`."
.help-options--restore:
@echo "PREPARE (alias: PREP)"
@echo "Run migrations after restoring the snapshot"
restore: $(SNAPSHOT_BINARY)
@ $(SNAPSHOT_BINARY) restore
@$(SNAPSHOT_BINARY) restore
@if [[ "$(PREP)" == "true" || "$(PREPARE)" == "true" ]]; then \
echo "Running migrations..."; \
./build/fleet prepare db --dev; \
fi
@echo Done!
# Generate osqueryd.app.tar.gz bundle from osquery.io.
#
@ -679,7 +772,4 @@ db-replica-reset: fleet
db-replica-run: fleet
FLEET_MYSQL_ADDRESS=127.0.0.1:3308 FLEET_MYSQL_READ_REPLICA_ADDRESS=127.0.0.1:3309 FLEET_MYSQL_READ_REPLICA_USERNAME=fleet FLEET_MYSQL_READ_REPLICA_DATABASE=fleet FLEET_MYSQL_READ_REPLICA_PASSWORD=insecure ./build/fleet serve --dev --dev_license
include ./tools/makefile-support/helpsystem-targets
foo:
@echo $(MAKECMDGOALS)
include ./tools/makefile-support/helpsystem-targets

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_NAME="${1:-backup.sql.gz}"
docker run --rm -i --network fleet_default ${FLEET_MYSQL_IMAGE:-mysql:8.0.36} bash -c 'gzip -dc - | mysql -hmysql -uroot -ptoor fleet' < ${BACKUP_NAME}
docker run --rm -i --network fleet_default ${FLEET_MYSQL_IMAGE:-mysql:8.0.36} bash -c 'gzip -dc - | MYSQL_PWD=toor mysql -hmysql -uroot fleet' < ${BACKUP_NAME}

View file

@ -67,6 +67,7 @@ func main() {
func splitArgs(args []string) (map[string]string, []string) {
options := make(map[string]string)
var makeArgs []string
cliArgs := " "
positionalArgsIndex := 1
isMakeArgs := false
skipNext := false
@ -96,13 +97,16 @@ func splitArgs(args []string) (map[string]string, []string) {
// Handle options like --name=foo
case len(parts) == 2:
options[parts[0]] = parts[1]
cliArgs += arg + " "
// Handle options like --name foo
case idx+1 < len(args) && !strings.HasPrefix(args[idx+1], "--"):
options[arg[2:]] = args[idx+1]
cliArgs += arg + " " + args[idx+1] + " "
skipNext = true
// Handle options like --useturbocharge by assuming they're booleans.
default:
options[parts[0]] = "true"
cliArgs += arg + " "
}
// Otherwise assume we're dealing with a positional argument.
default:
@ -110,7 +114,7 @@ func splitArgs(args []string) (map[string]string, []string) {
positionalArgsIndex++
}
}
options["CLI_ARGS"] = cliArgs
return options, makeArgs
}

View file

@ -94,6 +94,7 @@ func restore(homedir string) error {
// Walk the ~/.fleet/snapshots directory if it exists.
dirEntries, err := os.ReadDir(snapshotsDir)
var snapshots []Snapshot
var lastSnapshotName []byte
for _, entry := range dirEntries {
if entry.IsDir() {
// Ensure there's a db backup file.
@ -108,20 +109,42 @@ func restore(homedir string) error {
Path: dbBackupFile,
}
snapshots = append(snapshots, snapshot)
} else if entry.Name() == "last_snapshot" {
// If the entry is the "last_snapshot" file, read its contents
lastSnapshotPath := filepath.Join(snapshotsDir, entry.Name())
lastSnapshotName, err = os.ReadFile(lastSnapshotPath)
if err != nil {
fmt.Printf("Error reading last snapshot file (%s): %v\n", lastSnapshotPath, err)
return err
}
fmt.Println("Last snapshot: " + string(lastSnapshotName))
}
}
// If lastSnapshotName is not empty, find its index in the snapshots list.
var lastSnapshotIndex int
if len(lastSnapshotName) > 0 {
for i, snapshot := range snapshots {
if snapshot.Name == string(lastSnapshotName) {
lastSnapshotIndex = i
break
}
}
}
// Set up and run the "Select snapshot" UI.
templates := &promptui.SelectTemplates{
Label: "{{ .Name }}",
Active: "> {{ .Name }} ({{ .Date }})",
Inactive: "{{ .Name }} ({{ .Date }})",
Selected: "{{ .Name }} ({{ .Date }})",
Label: " {{ .Name }}",
Active: " {{ .Name }} ({{ .Date }})",
Inactive: " {{ .Name }} ({{ .Date }})",
Selected: " {{ .Name }} ({{ .Date }})",
}
prompt := promptui.Select{
Label: "Select snapshot to restore",
Items: snapshots,
Templates: templates,
Size: 10,
CursorPos: lastSnapshotIndex,
}
index, _, err := prompt.Run()
if err != nil {
@ -146,6 +169,12 @@ func restore(homedir string) error {
return err
}
// Write the selected snapshot name to the "last_snapshot" file.
err = os.WriteFile(filepath.Join(snapshotsDir, "last_snapshot"), []byte(snapshots[index].Name), 0o644)
if err != nil {
fmt.Printf("Error writing last snapshot file: %v\n", err)
}
return nil
}
@ -229,6 +258,12 @@ func snapshot(homedir string) error {
return err
}
// Write the selected snapshot name to the "last_snapshot" file.
err = os.WriteFile(filepath.Join(snapshotsDir, "last_snapshot"), []byte(result), 0o644)
if err != nil {
fmt.Printf("Error writing last snapshot file: %v\n", err)
}
return nil
}