mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
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
289 lines
7.6 KiB
Go
289 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
"github.com/manifoldco/promptui"
|
|
// Force promptui to use our newer x/sys package,
|
|
// which doesn't have security vulnerabilities.
|
|
_ "golang.org/x/sys/unix"
|
|
)
|
|
|
|
// Represents a snapshot.
|
|
// Snapshots are stored in folders named after the snapshot name.
|
|
// Each snapshot folder contains a db.sql.gz file.
|
|
type Snapshot struct {
|
|
Name string
|
|
Date string
|
|
Path string // The directory containing the snapshot.
|
|
}
|
|
|
|
// Which command to run.
|
|
type Command int
|
|
|
|
const (
|
|
CMD_SNAPSHOT Command = iota
|
|
CMD_RESTORE
|
|
)
|
|
|
|
func main() {
|
|
// Ensure there's a command specified.
|
|
// TODO - as we add more commands, we should probably use a library like spf13/cobra.
|
|
if len(os.Args) < 2 {
|
|
fmt.Println("Please specify whether to (b)ackup or (r)estore.")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Determine the command.
|
|
var command Command
|
|
switch os.Args[1] {
|
|
case "s", "snap", "snapshot":
|
|
command = CMD_SNAPSHOT
|
|
case "r", "restore":
|
|
command = CMD_RESTORE
|
|
default:
|
|
fmt.Println("Please specify whether to (s)snapshot or (r)estore.")
|
|
}
|
|
|
|
// Determine the path to the top-level directory (where the Makefile resides).
|
|
repoRoot, err := getRepoRoot()
|
|
if err != nil {
|
|
fmt.Printf("Error determining repo root: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Change the working directory to the repo root.
|
|
if err := os.Chdir(repoRoot); err != nil {
|
|
fmt.Printf("Error changing directory to repo root: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Get the home directory so we can get the snapshots dir.
|
|
homedir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
fmt.Printf("Could not determine home directory: %v\n", err)
|
|
return
|
|
}
|
|
|
|
// Run the command.
|
|
switch command {
|
|
case CMD_SNAPSHOT:
|
|
snapshot(homedir)
|
|
case CMD_RESTORE:
|
|
restore(homedir)
|
|
}
|
|
}
|
|
|
|
// Restore a snapshot.
|
|
func restore(homedir string) error {
|
|
snapshotsDir := filepath.Join(homedir, ".fleet", "snapshots")
|
|
_, err := os.Lstat(snapshotsDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
fmt.Printf("You don't currently have any snapshots.\n")
|
|
} else {
|
|
// Handle other PathError-specific cases
|
|
fmt.Printf("Error reading snapshots directory (%s): %v\n", snapshotsDir, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// 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.
|
|
dbBackupFile := filepath.Join(snapshotsDir, entry.Name(), "db.sql.gz")
|
|
dbBackupFileInfo, err := os.Lstat(dbBackupFile)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
snapshot := Snapshot{
|
|
Name: entry.Name(),
|
|
Date: dbBackupFileInfo.ModTime().Format("Jan 02, 2006 03:04:05 PM"),
|
|
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 }})",
|
|
}
|
|
prompt := promptui.Select{
|
|
Label: "Select snapshot to restore",
|
|
Items: snapshots,
|
|
Templates: templates,
|
|
Size: 10,
|
|
CursorPos: lastSnapshotIndex,
|
|
}
|
|
index, _, err := prompt.Run()
|
|
if err != nil {
|
|
fmt.Printf("Prompt failed %v\n", err)
|
|
return err
|
|
}
|
|
|
|
// Prepare the restore script with the selected snapshot.
|
|
cmd := exec.Command("./tools/backup_db/restore.sh", snapshots[index].Path)
|
|
|
|
// Use the same stdin, stdout, and stderr as the parent process.
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
// Run the command.
|
|
err = cmd.Run()
|
|
output, _ := cmd.CombinedOutput()
|
|
fmt.Println(string(output))
|
|
if err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
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
|
|
}
|
|
|
|
// Create a snapshot.
|
|
func snapshot(homedir string) error {
|
|
snapshotsDir := filepath.Join(homedir, ".fleet", "snapshots")
|
|
|
|
// Ensure the snapshots directory exists.
|
|
_, err := os.Lstat(snapshotsDir)
|
|
if err != nil {
|
|
// If the directory doesn't exist, create it.
|
|
if os.IsNotExist(err) {
|
|
err = os.Mkdir(snapshotsDir, 0o755)
|
|
if err != nil {
|
|
fmt.Printf("Error creating snapshots directory (%s): %v\n", snapshotsDir, err)
|
|
}
|
|
} else {
|
|
fmt.Printf("Error reading snapshots directory (%s): %v\n", snapshotsDir, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Prompt the user for a name for the snapshot.
|
|
prompt := promptui.Prompt{
|
|
Label: "Enter a name for the snapshot",
|
|
}
|
|
result, err := prompt.Run()
|
|
if err != nil {
|
|
fmt.Printf("Prompt failed %v\n", err)
|
|
return err
|
|
}
|
|
snapshotPath := filepath.Join(snapshotsDir, result)
|
|
|
|
// Check if the snapshot already exists.
|
|
_, err = os.Lstat(snapshotPath)
|
|
// If the file exists, prompt the user to overwrite it.
|
|
if err == nil {
|
|
prompt := promptui.Prompt{
|
|
Label: "This snapshot already exists. Overwrite? (Y/n)",
|
|
}
|
|
result, err := prompt.Run()
|
|
if err != nil {
|
|
fmt.Printf("Prompt failed %v\n", err)
|
|
return err
|
|
}
|
|
switch result {
|
|
case "Y", "y", "":
|
|
err = os.Remove(filepath.Join(snapshotPath, "db.sql.gz"))
|
|
if err != nil {
|
|
fmt.Printf("Error removing existing snapshot (%s): %v\n", result, err)
|
|
return err
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
fmt.Printf("Error checking for existing snapshot (%s): %v\n", result, err)
|
|
return err
|
|
}
|
|
|
|
// Create the snapshot directory
|
|
err = os.Mkdir(snapshotPath, 0o755)
|
|
if err != nil && !os.IsExist(err) {
|
|
fmt.Printf("Error creating snapshot directory (%s): %v\n", snapshotPath, err)
|
|
}
|
|
|
|
// Prepare the backup script with the snapshot path.
|
|
cmd := exec.Command("./tools/backup_db/backup.sh", filepath.Join(snapshotPath, "db.sql.gz"))
|
|
|
|
// Use the same stdin, stdout, and stderr as the parent process.
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
// Run the command.
|
|
err = cmd.Run()
|
|
output, _ := cmd.CombinedOutput()
|
|
fmt.Println(string(output))
|
|
if err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
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
|
|
}
|
|
|
|
// getRepoRoot determines the repo root (top-level directory) relative to this binary.
|
|
func getRepoRoot() (string, error) {
|
|
// Get the path of the currently executing binary
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Get the path of the binary, following symlinks.
|
|
execDir, err := filepath.EvalSymlinks(executable)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Get the directory.
|
|
execDir = filepath.Dir(execDir)
|
|
|
|
// Compute the repo root relative to the binary's location.
|
|
repoRoot := filepath.Join(execDir, "../")
|
|
return filepath.Abs(repoRoot)
|
|
}
|