From 75c1225db48c4b9c0959ca554894b8e8613f02ab Mon Sep 17 00:00:00 2001 From: fgma <30936930+fgma@users.noreply.github.com> Date: Sun, 5 May 2019 20:30:43 +0200 Subject: [PATCH] support for backups to multiple repositories --- README.rst | 25 ++++++---- TODO | 3 +- cmd/age.go | 40 +++++++++------- cmd/backup.go | 37 ++------------- cmd/backups.go | 4 +- cmd/check_age.go | 42 +++-------------- cmd/example_config.go | 10 ++-- cmd/init.go | 2 + cmd/root.go | 87 ++++++++++++++++++++++++++++++++++ cmd/shell.go | 2 +- cmd/version.go | 4 +- debian/changelog | 2 +- internal/config.go | 19 ++++++-- internal/config_test.go | 102 ++++++++++++++++++++++++++++++---------- internal/restic.go | 22 ++------- internal/util.go | 23 +++++++++ internal/util_test.go | 70 +++++++++++++++++++++++++++ 17 files changed, 339 insertions(+), 155 deletions(-) create mode 100644 internal/util_test.go diff --git a/README.rst b/README.rst index d241352..15ef817 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ System requirements Rester is implemented in golang and should run on all platforms restic supports. It only needs a recent version of restic. -Right now it is only tested running on linux. +Right now it is only tested running on linux and basic functionality on windows. Install ------- @@ -102,7 +102,7 @@ In addition to restic's forget command this will also run restic's prune command rester check-age -to check your backups ages. If you need to restore data you can use regular restic commands to do so or just mount a repository: +to check your backups ages. If everything is ok, restic will just exit with a exit code of 0 and no output. If you need to restore data you can use regular restic commands to do so or just mount a repository: .. code-block:: shell @@ -150,6 +150,16 @@ An overview of all available commands: Use "./rester [command] --help" for more information about a command. $ + +The commands ``backup`` and ``check-age`` support an advanced syntax for selecting backups to use: + +.. code-block:: shell + + rester backup my-configured-backup/one-of-its-repos another-backup + +This will run the backup ``my-configured-backup`` to repository ``one-of-its-repos`` and backup ``another-backup`` to all its configured repositories. + + .. _configuration: Configuration @@ -173,7 +183,7 @@ Repositories To actually backup data at least one repository has to be configured. Rester supports all repository formats restic supports. name - A unique name to refer to this repository. + A unique name to refer to this repository. Invalid characters: " ", "/". url The URL of the repository as passed to restic. For details on the format have a look at into restic's manual. @@ -225,7 +235,6 @@ handler If the commands start with a ``~`` sign it is expanded to the user's home directory. Additionally some special variables inside the commands are replaced with the appropriate values to automatically customize commands: - {{.BackupName}} - - {{.BackupRepository}} - {{.RepositoryName}} - {{.RepositoryURL}} @@ -241,7 +250,7 @@ Backups ======= name - A unique name to refer to this backup. + A unique name to refer to this backup. Invalid characters: " ", "/". repository The name of the repository to backup to as specified in the repositories section of the configuration. @@ -368,8 +377,8 @@ Example configuration ], "backups": [ { - "name": "/home/user", - "repository": "minio-backup", + "name": "home", + "repositories": [ "minio-backup" ], "data": [ "/home/user/" ], @@ -382,7 +391,7 @@ Example configuration }, { "name": "crontab", - "repository": "minio-backup", + "repositories": [ "minio-backup" ], "data_stdin_command": "crontab -l", "stdin_filename": "crontab.txt", "one_file_system": true, diff --git a/TODO b/TODO index e28e821..62481bc 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,3 @@ -- allow one backup to multiple repositories - - make consistent concerning commands like age, backup, check - add verbose flag to see what's going on - implement template based backups +- check exit codes for consistency diff --git a/cmd/age.go b/cmd/age.go index 48740fb..dd420ed 100644 --- a/cmd/age.go +++ b/cmd/age.go @@ -27,6 +27,7 @@ var ageCmd = &cobra.Command{ fmt.Fprintln(w, "----\t----\t----------\t---") now := time.Now() + exitCode := 0 for _, backup := range config.Backups { data := "" @@ -36,31 +37,34 @@ var ageCmd = &cobra.Command{ data = backup.DataStdinCommand } - repository := config.GetRepositoryByName(backup.Repository) + for _, repo := range backup.Repositories { + repository := config.GetRepositoryByName(repo) - if repository == nil { - fmt.Fprintf(os.Stderr, "Repository %s is not a configured repository\n", backup.Repository) - os.Exit(1) - } + if repository == nil { + fmt.Fprintf(os.Stderr, "Repository %s is not a configured repository\n", repo) + os.Exit(1) + } - lastBackupTimestamp, err := restic.GetLastBackupTimestamp(backup, *repository) + lastBackupTimestamp, err := restic.GetLastBackupTimestamp(backup, *repository) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to get age for backup %s: %s\n", backup.Name, err) - os.Exit(1) - } + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get age for backup %s: %s\n", backup.Name, err) + exitCode = 1 + } - age := "-" - if (lastBackupTimestamp != time.Time{}) { - age = now.Sub(lastBackupTimestamp).String() - } + age := "-" + if (lastBackupTimestamp != time.Time{}) { + age = now.Sub(lastBackupTimestamp).String() + } - fmt.Fprintf( - w, "%s\t%s\t%s\t%s\n", - backup.Name, data, backup.Repository, age, - ) + fmt.Fprintf( + w, "%s\t%s\t%s\t%s\n", + backup.Name, data, repository.Name, age, + ) + } } w.Flush() + os.Exit(exitCode) }, } diff --git a/cmd/backup.go b/cmd/backup.go index 7765c7c..091e89a 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -17,23 +17,11 @@ var backupCmd = &cobra.Command{ Long: `Run backups specified on the commandline or all if no backup is specified`, Args: cobra.ArbitraryArgs, Run: func(cmd *cobra.Command, args []string) { - - ensureBackupsExist(args) - - if len(args) == 0 { - for _, backup := range config.Backups { - runBackup(backup.Name) - } - } else { - for _, backupName := range args { - runBackup(backupName) - } - } - + runForBackupConfigurations(args, runBackup) }, } -func runBackup(backupName string) { +func runBackup(backupName string, repositoryName string) (int, error) { backup := config.GetBackupByName(backupName) @@ -42,31 +30,16 @@ func runBackup(backupName string) { os.Exit(1) } - repository := config.GetRepositoryByName(backup.Repository) + repository := config.GetRepositoryByName(repositoryName) if repository == nil { - fmt.Fprintf(os.Stderr, "Repository %s is not a configured repository\n", backup.Repository) + fmt.Fprintf(os.Stderr, "Repository %s is not a configured repository\n", repositoryName) os.Exit(1) } if err := restic.RunBackup(*backup, *repository); err != nil { fmt.Fprintf(os.Stderr, "Backup %s failed to run: %s\n", backupName, err.Error()) } -} - -func ensureBackupsExist(backups []string) { - for _, backupName := range backups { - isExistingBackup := false - for _, b := range config.Backups { - if b.Name == backupName { - isExistingBackup = true - break - } - } - if !isExistingBackup { - fmt.Fprintf(os.Stderr, "%s is not a configured backup\n", backupName) - os.Exit(1) - } - } + return 0, nil } diff --git a/cmd/backups.go b/cmd/backups.go index 2c56579..7d395cd 100644 --- a/cmd/backups.go +++ b/cmd/backups.go @@ -22,7 +22,7 @@ var backupsCmd = &cobra.Command{ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintln(w, "name\tdata\trepository") + fmt.Fprintln(w, "name\tdata\trepositories") fmt.Fprintln(w, "----\t----\t----------") for _, backup := range config.Backups { @@ -34,7 +34,7 @@ var backupsCmd = &cobra.Command{ } fmt.Fprintf( w, "%s\t%s\t%s\n", - backup.Name, data, backup.Repository, + backup.Name, data, strings.Join(backup.Repositories, ","), ) } diff --git a/cmd/check_age.go b/cmd/check_age.go index 93775d4..ece11f6 100644 --- a/cmd/check_age.go +++ b/cmd/check_age.go @@ -17,39 +17,11 @@ var checkAgeCmd = &cobra.Command{ Long: `Check age of the given backups`, Args: cobra.ArbitraryArgs, Run: func(cmd *cobra.Command, args []string) { - - ensureBackupsExist(args) - - var error error = nil - var checkExitCode int = 0 - - if len(args) == 0 { - for _, backup := range config.Backups { - exitCode, err := runCheckAge(backup.Name) - if error == nil { - error = err - } - if exitCode > checkExitCode { - checkExitCode = exitCode - } - } - } else { - for _, backupName := range args { - exitCode, err := runCheckAge(backupName) - if error == nil { - error = err - } - if exitCode > checkExitCode { - checkExitCode = exitCode - } - } - } - - os.Exit(checkExitCode) + runForBackupConfigurations(args, runCheckAge) }, } -func runCheckAge(backupName string) (int, error) { +func runCheckAge(backupName string, repositoryName string) (int, error) { backup := config.GetBackupByName(backupName) @@ -58,10 +30,10 @@ func runCheckAge(backupName string) (int, error) { os.Exit(1) } - repository := config.GetRepositoryByName(backup.Repository) + repository := config.GetRepositoryByName(repositoryName) if repository == nil { - fmt.Fprintf(os.Stderr, "Repository %s is not a configured repository\n", backup.Repository) + fmt.Fprintf(os.Stderr, "Repository %s is not a configured repository\n", repositoryName) os.Exit(1) } @@ -69,13 +41,13 @@ func runCheckAge(backupName string) (int, error) { exitCode := 0 if err != nil { - fmt.Fprintf(os.Stderr, "Error checking age for backup %s to repository %s\n", backup.Name, backup.Repository) + fmt.Fprintf(os.Stderr, "Error checking age for backup %s to repository %s\n", backup.Name, repository.Name) exitCode = 1 } else if limitError { - fmt.Fprintf(os.Stderr, "Error limit reached for backup %s to repository %s\n", backup.Name, backup.Repository) + fmt.Fprintf(os.Stderr, "Error limit reached for backup %s to repository %s\n", backup.Name, repository.Name) exitCode = 3 } else if limitWarn { - fmt.Fprintf(os.Stdout, "Warning limit reached for backup %s to repository %s\n", backup.Name, backup.Repository) + fmt.Fprintf(os.Stdout, "Warning limit reached for backup %s to repository %s\n", backup.Name, repository.Name) exitCode = 2 } diff --git a/cmd/example_config.go b/cmd/example_config.go index 47be4f7..0f9c51f 100644 --- a/cmd/example_config.go +++ b/cmd/example_config.go @@ -33,8 +33,8 @@ var exampleConfigCmd = &cobra.Command{ ], "backups": [ { - "name": "some data", - "repository": "backup-repo", + "name": "some_data", + "repositories": [ "backup-repo" ], "data": [ "/home/testuser/pictures", "/home/testuser/data" @@ -51,7 +51,7 @@ var exampleConfigCmd = &cobra.Command{ }, { "name": "crontab", - "repository": "backup-repo", + "repositories": [ "backup-repo" ], "data_stdin_command": "crontab -l", "stdin_filename": "crontab.txt", "one_file_system": true, @@ -68,8 +68,8 @@ var exampleConfigCmd = &cobra.Command{ _, err := internal.LoadFromReader(reader) if err != nil { fmt.Fprintf(os.Stderr, "Failed to parse example config: %s", err) + } else { + fmt.Println(exampleConfig) } - - fmt.Println(exampleConfig) }, } diff --git a/cmd/init.go b/cmd/init.go index 531e835..a325991 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -18,6 +18,8 @@ var initCmd = &cobra.Command{ Args: cobra.ArbitraryArgs, Run: func(cmd *cobra.Command, args []string) { + ensureRepositoriesExist(args) + if len(args) == 0 { for _, repository := range config.Repositories { initRepository(repository.Name) diff --git a/cmd/root.go b/cmd/root.go index 8494ceb..ea15b3d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "github.com/fgma/rester/internal" homedir "github.com/mitchellh/go-homedir" @@ -80,6 +81,92 @@ func initConfig() { } } +func runForBackupConfigurations( + configurationsToRun []string, + handler func(backupName string, repoName string) (returnCode int, err error), +) { + + type Configuration struct { + backupName string + repoName string + } + + var configsToRun []Configuration + + if len(configurationsToRun) == 0 { + // if args are empty run all configurations + for _, backup := range config.Backups { + for _, repo := range backup.Repositories { + configsToRun = append(configsToRun, Configuration{backup.Name, repo}) + } + } + } else { + // otherwise run given configurations + for _, configurationName := range configurationsToRun { + + split := strings.Split(configurationName, "/") + + if len(split) == 1 { + // no specific repository given, run against all repositories + backupName := split[0] + backup := config.GetBackupByName(backupName) + + if backup == nil { + fmt.Fprintf(os.Stderr, "Backup %s is not a configured backup\n", backupName) + os.Exit(1) + } + + for _, repo := range backup.Repositories { + configsToRun = append(configsToRun, Configuration{backupName, repo}) + } + + } else if len(split) == 2 { + // specific repository given, just run against this repository + backupName := split[0] + repoName := split[1] + backup := config.GetBackupByName(backupName) + repo := config.GetRepositoryByName(repoName) + + if backup == nil { + fmt.Fprintf(os.Stderr, "Backup %s is not a configured backup\n", backupName) + os.Exit(1) + } + + if repo == nil { + fmt.Fprintf(os.Stderr, "Repository %s is not a configured repository\n", repoName) + os.Exit(1) + } + + if !internal.Contains(backup.Repositories, repo.Name) { + fmt.Fprintf(os.Stderr, "Repository %s is not a configured for backup %s\n", repo.Name, backup.Name) + os.Exit(1) + } + + configsToRun = append(configsToRun, Configuration{backup.Name, repo.Name}) + + } else { + fmt.Fprintf(os.Stderr, "Configuration %s is invalid\n", configurationName) + os.Exit(1) + } + + } + } + + finalExitCode := 0 + + for _, cfg := range configsToRun { + exitCode, err := handler(cfg.backupName, cfg.repoName) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + } + if exitCode > finalExitCode { + finalExitCode = exitCode + } + } + + os.Exit(finalExitCode) +} + func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/cmd/shell.go b/cmd/shell.go index 644fb8e..21e4be5 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -30,7 +30,7 @@ var shellCmd = &cobra.Command{ shell, err := loginshell.Shell() if err != nil { - fmt.Fprintf(os.Stderr, "Failed to get shell loginshell: %s\n", err) + fmt.Fprintf(os.Stderr, "Failed to get shell: %s\n", err) os.Exit(1) } diff --git a/cmd/version.go b/cmd/version.go index 3b6fd19..28e9ca0 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -10,13 +10,13 @@ func init() { rootCmd.AddCommand(versionCmd) } -var versionRevision string = "dev" +var versionRevision = "dev" var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number", Long: `Print the version number"`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("rester v0.1.0 -- " + versionRevision) + fmt.Println("rester v0.1.1 -- " + versionRevision) }, } diff --git a/debian/changelog b/debian/changelog index d3f8b6b..43e92ed 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -rester (0.1.0-1) unstable; urgency=medium +rester (0.1.1-1) unstable; urgency=medium * Initial release. diff --git a/internal/config.go b/internal/config.go index 81c3c9e..ab8c938 100644 --- a/internal/config.go +++ b/internal/config.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" "runtime" + "strings" jsonutil "github.com/vrischmann/jsonutil" ) @@ -70,7 +71,7 @@ type backupDefaultable struct { type Backup struct { Name string `json:"name,omitempty"` - Repository string `json:"repository,omitempty"` + Repositories []string `json:"repositories,omitempty"` Data []string `json:"data,omitempty"` DataStdinCommand string `json:"data_stdin_command,omitempty"` StdinFilename string `json:"stdin_filename,omitempty"` @@ -262,6 +263,10 @@ func validateRepository(repo *Repository) error { return ValidationError{"Repository has no name."} } + if strings.ContainsAny(repo.Name, "\\ ") { + return ValidationError{"Repository name contains invalid character."} + } + if repo.URL == "" { return ValidationError{"Repository has no URL."} } @@ -283,12 +288,18 @@ func validateBackup(backup *Backup, repoNames map[string]bool) error { return ValidationError{"Backup has no name."} } - if backup.Repository == "" { + if strings.ContainsAny(backup.Name, "\\ ") { + return ValidationError{"Backup name contains invalid character."} + } + + if len(backup.Repositories) == 0 { return ValidationError{"Backup has no repository."} } - if _, ok := repoNames[backup.Repository]; !ok { - return ValidationError{fmt.Sprintf("Backup repository %s not defined.", backup.Repository)} + for _, repo := range backup.Repositories { + if _, ok := repoNames[repo]; !ok { + return ValidationError{fmt.Sprintf("Backup repository %s not defined.", repo)} + } } if len(backup.Data) > 0 && backup.DataStdinCommand != "" { diff --git a/internal/config_test.go b/internal/config_test.go index 77ece33..fe4d85f 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -33,8 +33,8 @@ func TestLoadConfig(t *testing.T) { ], "backups": [ { - "name": "some data", - "repository": "test1", + "name": "some_data", + "repositories": [ "test1" ], "data": [ "/etc/", "/var/lib/", @@ -53,14 +53,14 @@ func TestLoadConfig(t *testing.T) { }, { "name": "mysql", - "repository": "test2", + "repositories": [ "test2" ], "data_stdin_command": "mysqldump", "stdin_filename": "mysqldump.sql", "tags": [ "db", "mysql" ] }, { - "name": "some other data", - "repository": "test2", + "name": "some_other_data", + "repositories": [ "test2", "test1" ], "data": [ "/etc/" ], "exclude": [ "*.tmp", "*.bcd" ], "one_file_system": false, @@ -100,8 +100,9 @@ func TestLoadConfig(t *testing.T) { // // backup 0 // - assert.Equal(t, "some data", config.Backups[0].Name) - assert.Equal(t, "test1", config.Backups[0].Repository) + assert.Equal(t, "some_data", config.Backups[0].Name) + assert.Equal(t, 1, len(config.Backups[0].Repositories)) + assert.Equal(t, "test1", config.Backups[0].Repositories[0]) assert.Equal(t, 3, len(config.Backups[0].Data)) assert.Equal(t, "/etc/", config.Backups[0].Data[0]) @@ -130,7 +131,8 @@ func TestLoadConfig(t *testing.T) { // backup 1 // assert.Equal(t, "mysql", config.Backups[1].Name) - assert.Equal(t, "test2", config.Backups[1].Repository) + assert.Equal(t, 1, len(config.Backups[1].Repositories)) + assert.Equal(t, "test2", config.Backups[1].Repositories[0]) assert.Equal(t, "mysqldump", config.Backups[1].DataStdinCommand) assert.Equal(t, "mysqldump.sql", config.Backups[1].StdinFilename) assert.Equal(t, false, config.Backups[1].OneFileSystem) @@ -147,8 +149,10 @@ func TestLoadConfig(t *testing.T) { // // backup 2 // - assert.Equal(t, "some other data", config.Backups[2].Name) - assert.Equal(t, "test2", config.Backups[2].Repository) + assert.Equal(t, "some_other_data", config.Backups[2].Name) + assert.Equal(t, 2, len(config.Backups[2].Repositories)) + assert.Equal(t, "test2", config.Backups[2].Repositories[0]) + assert.Equal(t, "test1", config.Backups[2].Repositories[1]) assert.Equal(t, 1, len(config.Backups[2].Data)) assert.Equal(t, "/etc/", config.Backups[2].Data[0]) @@ -181,7 +185,7 @@ func TestLoadConfigDataAndStdinShoulFail(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data": [ "/etc/" ], "data_stdin_command": "mysqldump", "tags": [ "db", "mysql" ] @@ -206,7 +210,7 @@ func TestLoadConfigDataWithouthStdinFilenameShoulFail(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data_stdin_command": "mysqldump", "tags": [ "db", "mysql" ] } @@ -230,7 +234,7 @@ func TestLoadConfigNothingToBackup(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "tags": [ "db", "mysql" ] } ] @@ -253,7 +257,7 @@ func TestLoadConfigInvalidRepostory(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "INVALID REPOSITORY", + "repositories": [ "INVALID REPOSITORY" ], "data_stdin_command": "mysqldump" } ] @@ -264,6 +268,52 @@ func TestLoadConfigInvalidRepostory(t *testing.T) { assert.True(t, strings.Contains(error.Error(), "Backup repository INVALID REPOSITORY not defined.")) } +func TestLoadConfigInvalidRepositoryName(t *testing.T) { + reader := strings.NewReader(`{ + "repositories": [ + { + "name": "test1/ ", + "url": "/home/test/repos/test1", + "password": "1" + } + ], + "backups": [ + { + "name": "mysql", + "repositories": [ "test1" ], + "data_stdin_command": "mysqldump" + } + ] + }`) + + _, error := LoadFromReader(reader) + assert.True(t, error != nil) + assert.True(t, strings.Contains(error.Error(), "Repository name contains invalid character.")) +} + +func TestLoadConfigInvalidBackupName(t *testing.T) { + reader := strings.NewReader(`{ + "repositories": [ + { + "name": "test1", + "url": "/home/test/repos/test1", + "password": "1" + } + ], + "backups": [ + { + "name": "mysql/ 123", + "repositories": [ "test1" ], + "data_stdin_command": "mysqldump" + } + ] + }`) + + _, error := LoadFromReader(reader) + assert.True(t, error != nil) + assert.True(t, strings.Contains(error.Error(), "Backup name contains invalid character.")) +} + func TestGetRepositoryByName(t *testing.T) { reader := strings.NewReader(`{ "repositories": [ @@ -313,13 +363,13 @@ func TestGetBackupByName(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data_stdin_command": "mysqldump", "stdin_filename": "mysqldump.sql" }, { - "name": "some data", - "repository": "test1", + "name": "some_data", + "repositories": [ "test1" ], "data_stdin_command": "mysqldump", "stdin_filename": "some_data.sql" } @@ -334,9 +384,9 @@ func TestGetBackupByName(t *testing.T) { assert.NotZero(t, mysql) assert.Equal(t, "mysql", mysql.Name) - someData := config.GetBackupByName("some data") + someData := config.GetBackupByName("some_data") assert.NotZero(t, someData) - assert.Equal(t, "some data", someData.Name) + assert.Equal(t, "some_data", someData.Name) assert.Zero(t, config.GetBackupByName("undefined")) } @@ -354,7 +404,7 @@ func TestLoadConfigWithResticExecutable(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data": [ "/etc/" ], "tags": [ "db", "mysql" ] } @@ -382,7 +432,7 @@ func TestLoadConfigWithCustomEnvironment(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data": [ "/etc/" ], "tags": [ "db", "mysql" ], "environment": { @@ -449,7 +499,7 @@ func TestLoadConfigWithAge(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data": [ "/etc/" ], "tags": [ "db", "mysql" ], "age": { @@ -483,7 +533,7 @@ func TestLoadConfigWithAgeHandler(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data": [ "/etc/" ], "tags": [ "db", "mysql" ], "age": { @@ -524,7 +574,7 @@ func TestLoadConfigWithAgeWarnLimitAboveErrorLimitShouldFail(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data": [ "/etc/" ], "tags": [ "db", "mysql" ], "age": { @@ -590,7 +640,7 @@ func TestLoadConfigWithDefaults(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data": [ "/etc/" ], "tags": [ "db", "mysql" ] } @@ -678,7 +728,7 @@ func TestLoadConfigWithSomeDefaults(t *testing.T) { "backups": [ { "name": "mysql", - "repository": "test1", + "repositories": [ "test1" ], "data": [ "/etc/" ], "tags": [ "db", "mysql" ], "handler": { diff --git a/internal/restic.go b/internal/restic.go index 1088d8a..5681750 100644 --- a/internal/restic.go +++ b/internal/restic.go @@ -512,16 +512,14 @@ func runHandler(command string, handlerName string, environment map[string]strin } else { type TemplateArgs struct { - BackupName string - BackupRepository string - RepositoryName string - RepositoryURL string + BackupName string + RepositoryName string + RepositoryURL string } args := TemplateArgs{} if backup != nil { args.BackupName = backup.Name - args.BackupRepository = backup.Repository } if repository != nil { args.RepositoryName = repository.Name @@ -605,17 +603,3 @@ func convertEnvironment(env map[string]string) []string { return result } - -func combineMaps(a, b map[string]string) map[string]string { - result := make(map[string]string) - - for k, v := range a { - result[k] = v - } - - for k, v := range b { - result[k] = v - } - - return result -} diff --git a/internal/util.go b/internal/util.go index 64df73e..f71f85b 100644 --- a/internal/util.go +++ b/internal/util.go @@ -25,3 +25,26 @@ func compareStringList(a, b []string) bool { } return true } + +func combineMaps(a, b map[string]string) map[string]string { + result := make(map[string]string) + + for k, v := range a { + result[k] = v + } + + for k, v := range b { + result[k] = v + } + + return result +} + +func Contains(slice []string, element string) bool { + for _, x := range slice { + if x == element { + return true + } + } + return false +} diff --git a/internal/util_test.go b/internal/util_test.go new file mode 100644 index 0000000..1b1f0cc --- /dev/null +++ b/internal/util_test.go @@ -0,0 +1,70 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestComparePathList(t *testing.T) { + assert.True(t, comparePathList([]string{}, []string{})) + + assert.False(t, comparePathList([]string{"/test1", "/test2"}, []string{"/test1"})) + + assert.True(t, comparePathList([]string{"/test1", "/test2"}, []string{"/test1", "/test2"})) + + assert.False(t, comparePathList([]string{}, []string{"/test1", "/test2"})) + assert.False(t, comparePathList([]string{"/test1", "/test2"}, []string{})) + + assert.False(t, comparePathList([]string{"/test1", "/test2"}, []string{"/test1", "/testX"})) +} + +func TestCompareStringList(t *testing.T) { + assert.True(t, compareStringList([]string{}, []string{})) + + assert.False(t, compareStringList([]string{"a", "b", "c"}, []string{"a", "b"})) + + assert.True(t, compareStringList([]string{"a", "b", "c"}, []string{"a", "b", "c"})) + + assert.False(t, compareStringList([]string{}, []string{"a", "b", "c"})) + assert.False(t, compareStringList([]string{"a", "b", "c"}, []string{})) + + assert.False(t, compareStringList([]string{"a", "b", "c"}, []string{"a", "b", "x"})) +} + +func TestCombineMaps(t *testing.T) { + + combined := combineMaps(map[string]string{}, map[string]string{}) + assert.Equal(t, 0, len(combined)) + + combined = combineMaps(map[string]string{ + "a": "1", + "b": "2", + }, map[string]string{}) + + assert.Equal(t, 2, len(combined)) + assert.Equal(t, "1", combined["a"]) + assert.Equal(t, "2", combined["b"]) + + combined = combineMaps(map[string]string{ + "a": "1", + "b": "2", + }, map[string]string{ + "c": "3", + "d": "4", + }) + + assert.Equal(t, 4, len(combined)) + assert.Equal(t, "1", combined["a"]) + assert.Equal(t, "2", combined["b"]) + assert.Equal(t, "3", combined["c"]) + assert.Equal(t, "4", combined["d"]) +} + +func TestContains(t *testing.T) { + assert.False(t, Contains([]string{}, "test")) + + assert.True(t, Contains([]string{"test1", "test2"}, "test1")) + assert.True(t, Contains([]string{"test1", "test2"}, "test2")) + assert.False(t, Contains([]string{"test1", "test2"}, "test")) +}