From a0db27be1af9917969d182e9a4fdb1c8c15aa27e Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Sat, 6 Jan 2024 14:29:14 -0800 Subject: [PATCH] Add ability to unlock repos that may have stale locks Defaults to remove all locks, even non-stale --- main.go | 32 ++++++++++++++++++++++++++++++-- restic.go | 16 ++++++++++++++++ restic_test.go | 18 ++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 0f8d697..a50feff 100644 --- a/main.go +++ b/main.go @@ -189,10 +189,32 @@ func runRestoreJobs(jobs []Job, names string, snapshot string) error { return filterJobErr } +func runUnlockJobs(jobs []Job, names string) error { + if names == "" { + return nil + } + + namesSlice := strings.Split(names, ",") + + if len(namesSlice) == 0 { + return nil + } + + jobs, filterJobErr := FilterJobs(jobs, namesSlice) + for _, job := range jobs { + if err := job.NewRestic().Unlock(UnlockOpts{RemoveAll: true}); err != nil { + return err + } + } + + return filterJobErr +} + type Flags struct { showVersion bool backup string restore string + unlock string restoreSnapshot string once bool healthCheckAddr string @@ -204,6 +226,7 @@ func readFlags() Flags { flag.BoolVar(&flags.showVersion, "version", false, "Display the version and exit") flag.StringVar(&flags.backup, "backup", "", "Run backup jobs now. Names are comma separated. `all` will run all.") flag.StringVar(&flags.restore, "restore", "", "Run restore jobs now. Names are comma separated. `all` will run all.") + flag.StringVar(&flags.unlock, "unlock", "", "Unlock job repos now. Names are comma separated. `all` will run all.") flag.BoolVar(&flags.once, "once", false, "Run jobs specified using -backup and -restore once and exit") flag.StringVar(&flags.healthCheckAddr, "addr", "0.0.0.0:8080", "address to bind health check API") flag.StringVar(&flags.metricsPushGateway, "push-gateway", "", "url of push gateway service for batch runs (optional)") @@ -214,7 +237,12 @@ func readFlags() Flags { return flags } -func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, snapshot string) error { +func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, unlockJobs, snapshot string) error { + // Run specified job unlocks + if err := runUnlockJobs(jobs, unlockJobs); err != nil { + return fmt.Errorf("Failed running unlock for jobs: %w", err) + } + // Run specified backup jobs if err := runBackupJobs(jobs, backupJobs); err != nil { return fmt.Errorf("Failed running backup jobs: %w", err) @@ -261,7 +289,7 @@ func main() { log.Fatalf("Failed to read jobs from files: %v", err) } - if err := runSpecifiedJobs(jobs, flags.backup, flags.restore, flags.restoreSnapshot); err != nil { + if err := runSpecifiedJobs(jobs, flags.backup, flags.restore, flags.unlock, flags.restoreSnapshot); err != nil { log.Fatal(err) } diff --git a/restic.go b/restic.go index 06a7675..94498ad 100644 --- a/restic.go +++ b/restic.go @@ -72,6 +72,16 @@ func (NoOpts) ToArgs() []string { return []string{} } +type UnlockOpts struct { + RemoveAll bool `hcl:"RemoveAll,optional"` +} + +func (uo UnlockOpts) ToArgs() (args []string) { + args = maybeAddArgBool(args, "--remove-all", uo.RemoveAll) + + return +} + type BackupOpts struct { Exclude []string `hcl:"Exclude,optional"` Include []string `hcl:"Include,optional"` @@ -333,6 +343,12 @@ func (rcmd Restic) Check() error { return err } +func (rcmd Restic) Unlock(unlockOpts UnlockOpts) error { + _, err := rcmd.RunRestic("unlock", unlockOpts) + + return err +} + type Snapshot struct { UID int `json:"uid"` GID int `json:"gid"` diff --git a/restic_test.go b/restic_test.go index b3fa80d..b11dd87 100644 --- a/restic_test.go +++ b/restic_test.go @@ -152,6 +152,20 @@ func TestForgetOpts(t *testing.T) { AssertEqual(t, "args didn't match", expected, args) } +func TestUnlockOpts(t *testing.T) { + t.Parallel() + + args := main.UnlockOpts{ + RemoveAll: true, + }.ToArgs() + + expected := []string{ + "--remove-all", + } + + AssertEqual(t, "args didn't match", expected, args) +} + func TestBuildEnv(t *testing.T) { t.Parallel() @@ -299,4 +313,8 @@ func TestResticInterface(t *testing.T) { value, err = os.ReadFile(restoredDataFile) AssertEqualFail(t, "unexpected error reading from test file", nil, err) AssertEqualFail(t, "incorrect value in test file", "testing", string(value)) + + // Try to unlock the repo (repo shouldn't really be locked, but this should still run without error + err = restic.Unlock(main.UnlockOpts{}) //nolint:exhaustruct + AssertEqualFail(t, "unexpected error unlocking repo", nil, err) }