diff --git a/cmd/microshift/main.go b/cmd/microshift/main.go index 89432b827b..a6b1702513 100644 --- a/cmd/microshift/main.go +++ b/cmd/microshift/main.go @@ -38,5 +38,6 @@ func newCommand() *cobra.Command { cmd.AddCommand(cmds.NewRunMicroshiftCommand()) cmd.AddCommand(cmds.NewVersionCommand(ioStreams)) cmd.AddCommand(cmds.NewShowConfigCommand(ioStreams)) + cmd.AddCommand(cmds.NewAdminCommand()) return cmd } diff --git a/go.mod b/go.mod index 39591a0835..8026ef4969 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/openshift/microshift -go 1.19 +go 1.20 require ( github.com/apparentlymart/go-cidr v1.1.0 diff --git a/pkg/admin/data/data_manager.go b/pkg/admin/data/data_manager.go new file mode 100644 index 0000000000..64fcbf4605 --- /dev/null +++ b/pkg/admin/data/data_manager.go @@ -0,0 +1,105 @@ +package data + +import ( + "bytes" + "fmt" + "os/exec" + "path/filepath" + "strings" + + "github.com/openshift/microshift/pkg/config" + "github.com/openshift/microshift/pkg/util" + "k8s.io/klog/v2" +) + +var ( + cpArgs = []string{ + "--verbose", + "--recursive", + "--preserve", + "--reflink=auto", + } +) + +func NewManager(storage StoragePath) (*manager, error) { + if storage == "" { + return nil, &EmptyArgErr{argName: "storage"} + } + return &manager{storage: storage}, nil +} + +var _ Manager = (*manager)(nil) + +type manager struct { + storage StoragePath +} + +func (dm *manager) GetBackupPath(name BackupName) string { + return filepath.Join(string(dm.storage), string(name)) +} + +func (dm *manager) BackupExists(name BackupName) (bool, error) { + return pathExists(dm.GetBackupPath(name)) +} + +func (dm *manager) Backup(name BackupName) error { + klog.InfoS("Backing up the data", + "storage", dm.storage, "name", name, "data", config.DataDir) + + if name == "" { + return &EmptyArgErr{"name"} + } + + if found, err := pathExists(string(dm.storage)); err != nil { + return err + } else if !found { + if makeDirErr := util.MakeDir(string(dm.storage)); makeDirErr != nil { + return fmt.Errorf("making %s directory failed: %w", dm.storage, makeDirErr) + } + klog.InfoS("Backup storage directory created", "path", dm.storage) + } else { + klog.InfoS("Backup storage directory already existed", "path", dm.storage) + } + + dest := dm.GetBackupPath(name) + + if err := copyDataDir(dest); err != nil { + return err + } + + klog.InfoS("Backup finished", "backup", dest, "data", config.DataDir) + return nil +} + +func (dm *manager) Restore(n BackupName) error { + return fmt.Errorf("Restore not implemented") +} + +func copyDataDir(dest string) error { + cmd := exec.Command("cp", append(cpArgs, config.DataDir, dest)...) //nolint:gosec + klog.InfoS("Executing command", "cmd", cmd) + + var outb, errb bytes.Buffer + cmd.Stdout = &outb + cmd.Stderr = &errb + err := cmd.Run() + + klog.InfoS("Command finished running", "cmd", cmd, + "stdout", strings.ReplaceAll(outb.String(), "\n", `, `), + "stderr", errb.String()) + + if err != nil { + return fmt.Errorf("command %s failed: %w", cmd, err) + } + + klog.InfoS("Command successful", "cmd", cmd) + return nil +} + +func pathExists(path string) (bool, error) { + exists, err := util.PathExists(path) + if err != nil { + return false, fmt.Errorf("checking if %s exists failed: %w", path, err) + } + return exists, nil +} diff --git a/pkg/admin/data/run_check.go b/pkg/admin/data/run_check.go new file mode 100644 index 0000000000..e3b353d3d3 --- /dev/null +++ b/pkg/admin/data/run_check.go @@ -0,0 +1,36 @@ +package data + +import ( + "fmt" + "os/exec" + "strings" + + "k8s.io/klog/v2" +) + +const ( + expectedState = "inactive" +) + +var ( + services = []string{"microshift.service", "microshift-etcd.scope"} +) + +func MicroShiftIsNotRunning() error { + for _, service := range services { + cmd := exec.Command("systemctl", "show", "-p", "ActiveState", "--value", service) + out, err := cmd.CombinedOutput() + state := strings.TrimSpace(string(out)) + if err != nil { + return fmt.Errorf("error when checking if %s is active: %w", service, err) + } + + klog.InfoS("Service state", "service", service, "state", state) + + if state != expectedState { + return fmt.Errorf("service %s is %s - expected to be %s", service, state, expectedState) + } + } + + return nil +} diff --git a/pkg/admin/data/types.go b/pkg/admin/data/types.go new file mode 100644 index 0000000000..d11d59d104 --- /dev/null +++ b/pkg/admin/data/types.go @@ -0,0 +1,24 @@ +package data + +import ( + "fmt" +) + +type EmptyArgErr struct { + argName string +} + +func (e *EmptyArgErr) Error() string { + return fmt.Sprintf("empty argument: %s", e.argName) +} + +type StoragePath string +type BackupName string + +type Manager interface { + Backup(BackupName) error + Restore(BackupName) error + + BackupExists(BackupName) (bool, error) + GetBackupPath(BackupName) string +} diff --git a/pkg/cmd/admin.go b/pkg/cmd/admin.go new file mode 100644 index 0000000000..117f62837f --- /dev/null +++ b/pkg/cmd/admin.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "fmt" + "time" + + "github.com/openshift/microshift/pkg/admin/data" + "github.com/openshift/microshift/pkg/config" + "github.com/openshift/microshift/pkg/version" + + "github.com/spf13/cobra" +) + +func backup(cmd *cobra.Command, args []string) error { + storage := data.StoragePath(cmd.Flag("storage").Value.String()) + name := data.BackupName(cmd.Flag("name").Value.String()) + + dataManager, err := data.NewManager(storage) + if err != nil { + return err + } + + if exists, err := dataManager.BackupExists(name); err != nil { + return err + } else if exists { + return fmt.Errorf("backup %s already exists", dataManager.GetBackupPath(name)) + } + + return dataManager.Backup(name) +} + +func newAdminDataCommand() *cobra.Command { + backup := &cobra.Command{ + Use: "backup", + Short: "Backup MicroShift data", + RunE: backup, + } + + data := &cobra.Command{ + Use: "data", + Short: "Commands for managing MicroShift data", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if cmd.Flag("storage").Value.String() == "" { + return fmt.Errorf("--storage must not be empty") + } + + if cmd.Flag("name").Value.String() == "" { + return fmt.Errorf("--name must not be empty") + } + + if err := data.MicroShiftIsNotRunning(); err != nil { + return fmt.Errorf("microshift must not be running: %w", err) + } + + return nil + }, + } + v := version.Get() + data.PersistentFlags().String("storage", config.BackupsDir, "Directory with backups") + data.PersistentFlags().String("name", + fmt.Sprintf("%s.%s__%s", v.Major, v.Minor, time.Now().UTC().Format("20060102_150405")), + "Backup name") + + data.AddCommand(backup) + return data +} + +func NewAdminCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "admin", + Short: "Commands for managing MicroShift", + } + cmd.AddCommand(newAdminDataCommand()) + return cmd +} diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 7e737f90a4..71e0946a46 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -99,7 +99,7 @@ func RunMicroshift(cfg *config.Config) error { klog.Fatal(err) } - if err := os.MkdirAll(config.DataDir, 0700); err != nil { + if err := util.MakeDir(config.DataDir); err != nil { return fmt.Errorf("failed to create dir %q: %w", config.DataDir, err) } diff --git a/pkg/config/files.go b/pkg/config/files.go index ffe7ee6d33..ff6fb3af27 100644 --- a/pkg/config/files.go +++ b/pkg/config/files.go @@ -10,6 +10,7 @@ import ( const ( ConfigFile = "/etc/microshift/config.yaml" DataDir = "/var/lib/microshift" + BackupsDir = "/var/lib/microshift-backups" ) func parse(contents []byte) (*Config, error) { diff --git a/pkg/util/util.go b/pkg/util/util.go index e2ad95c78e..61725b28d2 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1,7 +1,9 @@ package util import ( + "errors" "fmt" + "os" ) func Must(err error) { @@ -16,3 +18,17 @@ func Default(s string, defaultS string) string { } return s } + +func PathExists(path string) (bool, error) { + if _, err := os.Stat(path); err == nil { + return true, nil + } else if errors.Is(err, os.ErrNotExist) { + return false, nil + } else { + return false, err + } +} + +func MakeDir(path string) error { + return os.MkdirAll(path, 0700) +}