diff --git a/cmd/restore.go b/cmd/restore.go index 8faf4a7..2b748d9 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/cupcakearmy/autorestic/internal" + "github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/lock" "github.com/spf13/cobra" ) @@ -42,8 +43,13 @@ var restoreCmd = &cobra.Command{ } } - err = l.Restore(target, from, force, snapshot, optional) - CheckErr(err) + errs := l.Restore(target, from, force, snapshot, optional) + for _, err := range errs { + colors.Error.Printf("%s\n\n", err) + } + if len(errs) > 0 { + CheckErr(fmt.Errorf("%d errors were found", len(errs))) + } }, } diff --git a/docs/markdown/_toc.md b/docs/markdown/_toc.md index c50dc14..2858097 100644 --- a/docs/markdown/_toc.md +++ b/docs/markdown/_toc.md @@ -45,6 +45,7 @@ > > [0.x → 1.0](/migration/0.x_1.0) > [1.4 → 1.5](/migration/1.4_1.5) +> [1.7 → 1.8](/migration/1.7_1.8) [Examples](/examples) [Docker](/docker) diff --git a/docs/markdown/config.md b/docs/markdown/config.md index ee4e285..4e81851 100644 --- a/docs/markdown/config.md +++ b/docs/markdown/config.md @@ -55,10 +55,11 @@ version: 2 extras: hooks: &foo - before: - - echo "Hello" - after: - - echo "kthxbye" + backup: + before: + - echo "Hello" + after: + - echo "kthxbye" policies: &bar keep-daily: 14 keep-weekly: 52 diff --git a/docs/markdown/examples.md b/docs/markdown/examples.md index 9243bc9..94c5c30 100644 --- a/docs/markdown/examples.md +++ b/docs/markdown/examples.md @@ -22,12 +22,13 @@ autorestic exec -b my-backend -- unlock extras: healthchecks: &healthchecks hooks: - before: - - 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Starting backup for location: ${AUTORESTIC_LOCATION}" https:///ping//start' - failure: - - 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Backup failed for location: ${AUTORESTIC_LOCATION}" https:///ping//fail' - success: - - 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Backup successful for location: ${AUTORESTIC_LOCATION}" https:///ping/' + backup: + before: + - 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Starting backup for location: ${AUTORESTIC_LOCATION}" https:///ping//start' + failure: + - 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Backup failed for location: ${AUTORESTIC_LOCATION}" https:///ping//fail' + success: + - 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Backup successful for location: ${AUTORESTIC_LOCATION}" https:///ping/' locations: something: diff --git a/docs/markdown/location/hooks.md b/docs/markdown/location/hooks.md index a2bcf45..02dd19b 100644 --- a/docs/markdown/location/hooks.md +++ b/docs/markdown/location/hooks.md @@ -1,8 +1,8 @@ # Hooks -If you want to perform some commands before and/or after a backup, you can use hooks. +If you want to perform some commands before and/or after a backup or restore, you can use hooks. -They consist of a list of commands that will be executed in the same directory as the target `from`. +They consist of a list of commands that will be executed in the same directory as the config file or in the `dir` directory if configured. The following hooks groups are supported, none are required: @@ -17,16 +17,27 @@ locations: from: /data to: my-backend hooks: - before: - - echo "One" - - echo "Two" - - echo "Three" - after: - - echo "Byte" - failure: - - echo "Something went wrong" - success: - - echo "Well done!" + backup: + before: + - echo "One" + - echo "Two" + - echo "Three" + after: + - echo "Byte" + failure: + - echo "Something went wrong" + success: + - echo "Well done!" + restore: + dir: /var/www/html + before: + - echo "Let's restore this backup!" + after: + - echo "Finished to restore" + failure: + - echo "A problem has been encountered :(" + success: + - echo "Successfully restored!" ``` ## Flowchart diff --git a/docs/markdown/migration/1.7_1.8.md b/docs/markdown/migration/1.7_1.8.md new file mode 100644 index 0000000..e60779a --- /dev/null +++ b/docs/markdown/migration/1.7_1.8.md @@ -0,0 +1,45 @@ +# Migration from `1.7` to `1.8` + +## Config files + +- The config version have been changed +- You can now configure restore hooks + +See detailed instructions below. + +## Config Version + +The version field of the config file has been changed from `2` to `3`. + +## Hooks + +Since `1.8` both backup and restore hooks are possible. +For this reason, backup hooks have been moved one layer deeper, you have to move them in a `backup` object. + +Before: + +```yaml +locations: + l1: + # ... + from: /foo/bar + hooks: + before: + - pwd +``` + +After: + +```yaml +locations: + l1: + # ... + from: /foo/bar + hooks: + backup: + before: + - pwd + restore: + after: + - echo "My super restore hook" +``` diff --git a/docs/markdown/migration/index.md b/docs/markdown/migration/index.md index 624561d..e5621f5 100644 --- a/docs/markdown/migration/index.md +++ b/docs/markdown/migration/index.md @@ -2,3 +2,4 @@ - [From 0.x to 1.0](/migration/0.x_1.0) - [From 1.4 to 1.5](/migration/1.4_1.5) +- [From 1.7 to 1.8](/migration/1.7_1.8) diff --git a/internal/config.go b/internal/config.go index b66fb2f..877a75b 100644 --- a/internal/config.go +++ b/internal/config.go @@ -83,7 +83,7 @@ func GetConfig() *Config { exitConfig(nil, "version specified in config file is not an int") } else { // Check for version - if version != 2 { + if version != 3 { exitConfig(nil, "unsupported config version number. please check the docs for migration\nhttps://autorestic.vercel.app/migration/") } } @@ -132,10 +132,14 @@ func (c *Config) Describe() { tmp = "" hooks := map[string][]string{ - "Before": l.Hooks.Before, - "After": l.Hooks.After, - "Failure": l.Hooks.Failure, - "Success": l.Hooks.Success, + "Before backup": l.Hooks.BackupOption.Before, + "After backup": l.Hooks.BackupOption.After, + "Failure backup": l.Hooks.BackupOption.Failure, + "Success backup": l.Hooks.BackupOption.Success, + "Before restore": l.Hooks.RestoreOption.Before, + "After restore": l.Hooks.RestoreOption.After, + "Failure restore": l.Hooks.RestoreOption.Failure, + "Success restore": l.Hooks.RestoreOption.Success, } for hook, commands := range hooks { if len(commands) > 0 { diff --git a/internal/location.go b/internal/location.go index bf8deb5..4b91251 100644 --- a/internal/location.go +++ b/internal/location.go @@ -33,6 +33,13 @@ const ( ) type Hooks struct { + RestoreOption HooksList `mapstructure:"restore,omitempty"` + BackupOption HooksList `mapstructure:"backup,omitempty"` +} + +type LocationCopy = map[string][]string + +type HooksList struct { Dir string `mapstructure:"dir"` Before HookArray `mapstructure:"before,omitempty"` After HookArray `mapstructure:"after,omitempty"` @@ -40,18 +47,16 @@ type Hooks struct { Failure HookArray `mapstructure:"failure,omitempty"` } -type LocationCopy = map[string][]string - type Location struct { - name string `mapstructure:",omitempty"` - From []string `mapstructure:"from,omitempty"` - Type string `mapstructure:"type,omitempty"` - To []string `mapstructure:"to,omitempty"` - Hooks Hooks `mapstructure:"hooks,omitempty"` - Cron string `mapstructure:"cron,omitempty"` - Options Options `mapstructure:"options,omitempty"` - ForgetOption LocationForgetOption `mapstructure:"forget,omitempty"` - CopyOption LocationCopy `mapstructure:"copy,omitempty"` + name string `mapstructure:",omitempty"` + From []string `mapstructure:"from,omitempty"` + Type string `mapstructure:"type,omitempty"` + To []string `mapstructure:"to,omitempty"` + Hooks Hooks `mapstructure:"hooks,omitempty"` + Cron string `mapstructure:"cron,omitempty"` + Options Options `mapstructure:"options,omitempty"` + ForgetOption LocationForgetOption `mapstructure:"forget,omitempty"` + CopyOption LocationCopy `mapstructure:"copy,omitempty"` } func GetLocation(name string) (Location, bool) { @@ -123,12 +128,12 @@ func (l Location) validate() error { return nil } -func (l Location) ExecuteHooks(commands []string, options ExecuteOptions) error { +func (l Location) ExecuteHooks(commands []string, directory string, options ExecuteOptions) error { if len(commands) == 0 { return nil } - if l.Hooks.Dir != "" { - if dir, err := GetPathRelativeToConfig(l.Hooks.Dir); err != nil { + if directory != "" { + if dir, err := GetPathRelativeToConfig(directory); err != nil { return err } else { options.Dir = dir @@ -190,7 +195,7 @@ func (l Location) Backup(cron bool, specificBackend string) []error { } // Hooks - if err := l.ExecuteHooks(l.Hooks.Before, options); err != nil { + if err := l.ExecuteHooks(l.Hooks.BackupOption.Before, l.Hooks.BackupOption.Dir, options); err != nil { errors = append(errors, err) goto after } @@ -290,7 +295,7 @@ func (l Location) Backup(cron bool, specificBackend string) []error { } // After hooks - if err := l.ExecuteHooks(l.Hooks.After, options); err != nil { + if err := l.ExecuteHooks(l.Hooks.BackupOption.After, l.Hooks.BackupOption.Dir, options); err != nil { errors = append(errors, err) } @@ -298,11 +303,11 @@ after: var commands []string var isSuccess = len(errors) == 0 if isSuccess { - commands = l.Hooks.Success + commands = l.Hooks.BackupOption.Success } else { - commands = l.Hooks.Failure + commands = l.Hooks.BackupOption.Failure } - if err := l.ExecuteHooks(commands, options); err != nil { + if err := l.ExecuteHooks(commands, l.Hooks.BackupOption.Dir, options); err != nil { errors = append(errors, err) } @@ -370,11 +375,35 @@ func buildRestoreCommand(l Location, to string, snapshot string, options []strin return base } -func (l Location) Restore(to, from string, force bool, snapshot string, options []string) error { +func (l Location) Restore(to, from string, force bool, snapshot string, options []string) (errors []error) { + cwd, _ := GetPathRelativeToConfig(".") + hooksOptions := ExecuteOptions{ + Command: "bash", + Dir: cwd, + Envs: map[string]string{ + "AUTORESTIC_LOCATION": l.name, + }, + } + + defer func() { + var commands []string + var isSuccess = len(errors) == 0 + if isSuccess { + commands = l.Hooks.RestoreOption.Success + } else { + commands = l.Hooks.RestoreOption.Failure + } + if err := l.ExecuteHooks(commands, l.Hooks.RestoreOption.Dir, hooksOptions); err != nil { + errors = append(errors, err) + } + + colors.Success.Println("Done") + }() + if from == "" { from = l.To[0] } else if !l.hasBackend(from) { - return fmt.Errorf("invalid backend: \"%s\"", from) + errors = append(errors, fmt.Errorf("invalid backend: \"%s\"", from)) } if snapshot == "" { @@ -385,15 +414,23 @@ func (l Location) Restore(to, from string, force bool, snapshot string, options backend, _ := GetBackend(from) colors.Secondary.Printf("Restoring %s@%s → %s\n", snapshot, backend.name, to) + // Before Hooks for restore + if err := l.ExecuteHooks(l.Hooks.RestoreOption.Before, l.Hooks.RestoreOption.Dir, hooksOptions); err != nil { + errors = append(errors, err) + return + } + t, err := l.getType() if err != nil { - return err + errors = append(errors, err) + return } switch t { case TypeLocal: to, err = filepath.Abs(to) if err != nil { - return err + errors = append(errors, err) + return } // Check if target is empty if !force { @@ -402,14 +439,17 @@ func (l Location) Restore(to, from string, force bool, snapshot string, options if err == nil { files, err := ioutil.ReadDir(to) if err != nil { - return err + errors = append(errors, err) + return } if len(files) > 0 { - return notEmptyError + errors = append(errors, notEmptyError) + return } } else { if !os.IsNotExist(err) { - return err + errors = append(errors, err) + return } } } @@ -418,10 +458,17 @@ func (l Location) Restore(to, from string, force bool, snapshot string, options _, _, err = backend.ExecDocker(l, buildRestoreCommand(l, "/", snapshot, options)) } if err != nil { - return err + errors = append(errors, err) + return } - colors.Success.Println("Done") - return nil + + // After Hooks for restore + if err := l.ExecuteHooks(l.Hooks.RestoreOption.After, l.Hooks.RestoreOption.Dir, hooksOptions); err != nil { + errors = append(errors, err) + return + } + + return } func (l Location) RunCron() error {