Merge pull request #118 from cupcakearmy/1.4.0

1.4.0
This commit is contained in:
Nicco 2021-10-30 15:02:27 +02:00 committed by GitHub
commit de663f287c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 216 additions and 84 deletions

View File

@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.0] - 2021-10-30
### Added
- Allow specify to specify a backend for location backup
- Global restic flags
- Generic ENV support for backends
### Changed
- Install now only requires `wget`
- Env variable for the `KEY` has been renamed from `AUTORESTIC_[BACKEND NAME]_KEY` -> `AUTORESTIC_[BACKEND NAME]_RESTIC_PASSWORD`
### Fixed
- Error handling during upgrade & uninstall
## [1.3.0] - 2021-10-26 ## [1.3.0] - 2021-10-26
### Added ### Added

View File

@ -2,6 +2,7 @@ package cmd
import ( import (
"fmt" "fmt"
"strings"
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
@ -22,8 +23,13 @@ var backupCmd = &cobra.Command{
CheckErr(err) CheckErr(err)
errors := 0 errors := 0
for _, name := range selected { for _, name := range selected {
location, _ := internal.GetLocation(name) var splitted = strings.Split(name, "@")
errs := location.Backup(false) var specificBackend = ""
if len(splitted) > 1 {
specificBackend = splitted[1]
}
location, _ := internal.GetLocation(splitted[0])
errs := location.Backup(false, specificBackend)
for err := range errs { for err := range errs {
colors.Error.Println(err) colors.Error.Println(err)
errors++ errors++

View File

@ -9,12 +9,12 @@ var uninstallCmd = &cobra.Command{
Use: "uninstall", Use: "uninstall",
Short: "Uninstall restic and autorestic", Short: "Uninstall restic and autorestic",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
noRestic, _ := cmd.Flags().GetBool("no-restic") restic, _ := cmd.Flags().GetBool("restic")
bins.Uninstall(!noRestic) bins.Uninstall(restic)
}, },
} }
func init() { func init() {
rootCmd.AddCommand(uninstallCmd) rootCmd.AddCommand(uninstallCmd)
uninstallCmd.Flags().Bool("no-restic", false, "do not uninstall restic.") uninstallCmd.Flags().Bool("restic", false, "also uninstall restic.")
} }

View File

@ -9,13 +9,13 @@ var upgradeCmd = &cobra.Command{
Use: "upgrade", Use: "upgrade",
Short: "Upgrade autorestic and restic", Short: "Upgrade autorestic and restic",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
noRestic, _ := cmd.Flags().GetBool("no-restic") restic, _ := cmd.Flags().GetBool("restic")
err := bins.Upgrade(!noRestic) err := bins.Upgrade(restic)
CheckErr(err) CheckErr(err)
}, },
} }
func init() { func init() {
rootCmd.AddCommand(upgradeCmd) rootCmd.AddCommand(upgradeCmd)
upgradeCmd.Flags().Bool("no-restic", false, "also update restic") upgradeCmd.Flags().Bool("restic", true, "also update restic")
} }

View File

@ -4,6 +4,8 @@ In theory [all the restic backends](https://restic.readthedocs.io/en/stable/030_
Those tested are the following: Those tested are the following:
> You can also [specify the `env` variables in a config file](/backend/env) to separate them from the config file.
## Local ## Local
```yaml ```yaml

View File

@ -1,36 +1,67 @@
# Environment # Environment
> ⚠ Available since version `v1.3.0` > ⚠ Available since version `v1.4.0`
Sometimes it's favorable not having the encryption keys in the config files. Sometimes it's favorable not having the encryption keys in the config files.
For that `autorestic` allows passing the backend keys as `ENV` variables, or through an env file. For that `autorestic` allows passing the env variables to backend password as `ENV` variables, or through an env file.
You can also pass whatever `env` variable to restic by prefixing it with `AUTORESTIC_[BACKEND NAME]_`.
The syntax for the `ENV` variables is as follows: `AUTORESTIC_[BACKEND NAME]_KEY`. > Env variables and file overwrite the config file in the following order:
>
> Env Variables > Env File (`.autorestic.env`) > Config file (`.autorestic.yaml`)
## Env file
Alternatively `autorestic` can load an env file, located next to `.autorestic.yml` called `.autorestic.env`.
```
AUTORESTIC_FOO_RESTIC_PASSWORD=secret123
```
### Example with repository password
The syntax for the `ENV` variables is as follows: `AUTORESTIC_[BACKEND NAME]_RESTIC_PASSWORD`.
```yaml | autorestic.yaml ```yaml | autorestic.yaml
backend: backend:
foo: foo:
type: ... type: ...
path: ... path: ...
key: secret123 # => AUTORESTIC_FOO_KEY=secret123 key: secret123 # => AUTORESTIC_FOO_RESTIC_PASSWORD=secret123
``` ```
## Example
This means we could remove `key: secret123` from `.autorestic.yaml` and execute as follows: This means we could remove `key: secret123` from `.autorestic.yaml` and execute as follows:
```bash ```bash
AUTORESTIC_FOO_KEY=secret123 autorestic backup ... AUTORESTIC_FOO_RESTIC_PASSWORD=secret123 autorestic backup ...
``` ```
## Env file ### Example with Backblaze B2
Alternatively `autorestic` can load an env file, located next to `autorestic.yml` called `.autorestic.env`. ```yaml | autorestic.yaml
backends:
```| .autorestic.env bb:
AUTORESTIC_FOO_KEY=secret123 type: b2
path: myBucket
key: myPassword
env:
B2_ACCOUNT_ID: 123
B2_ACCOUNT_KEY: 456
``` ```
after that you can simply use `autorestic` as your are used to. You could create an `.autorestic.env` or pass the following `ENV` variables to autorestic:
```
AUTORESTIC_BB_RESTIC_PASSWORD=myPassword
AUTORESTIC_BB_B2_ACCOUNT_ID=123
AUTORESTIC_BB_B2_ACCOUNT_KEY=456
```
```yaml | autorestic.yaml
backends:
bb:
type: b2
path: myBucket
```
> :ToCPrevNext > :ToCPrevNext

View File

@ -1,8 +1,6 @@
# Options # Options
For the `backup` and `forget` commands you can pass any native flags to `restic`. > For more detail see the [location docs](/location/options) for options, as they are the same.
> It is also possible to set options for an [a specific location](/location/options).
```yaml ```yaml
backend: backend:
@ -18,8 +16,4 @@ backend:
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command. In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
For more detail see the [location docs](/location/options) for options, as they are the same.
> For flags without arguments you can set them to `true`. They will be handled accordingly.
> :ToCPrevNext > :ToCPrevNext

View File

@ -14,4 +14,12 @@ autorestic backup -a
autorestic backup -l foo -l bar autorestic backup -l foo -l bar
``` ```
## Specific location
`autorestic` also allows selecting specific backends for a location with the `location@backend` syntax.
```bash
autorestic backup -l location@backend
```
> :ToCPrevNext > :ToCPrevNext

View File

@ -2,10 +2,10 @@
Linux & macOS. Windows is not supported. If you have problems installing please open an issue :) Linux & macOS. Windows is not supported. If you have problems installing please open an issue :)
Autorestic requires `curl`, `wget` and `bzip2` to be installed. For most systems these should be already installed. Autorestic requires `bash`, `wget` and `bzip2` to be installed. For most systems these should be already installed.
```bash ```bash
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
``` ```
## Alternatives ## Alternatives

View File

@ -1,8 +1,32 @@
# Options # Options
For the `backup` and `forget` commands you can pass any native flags to `restic`. For the `backup` and `forget` commands you can pass any native flags to `restic`. In addition you can specify flags for every command with `all`.
> It is also possible to set options for an [entire backend](/backend/options). If flags don't start with `-` they will get prefixed with `--`.
Flags without arguments can be set to `true`. They will be handled accordingly.
> It is also possible to set options for an [entire backend](/backend/options) or globally (see below).
```yaml
locations:
foo:
# ...
options:
all:
some-flag: 123
# Equivalent to
--some-flag: 123
backup:
boolean-flag: true
tag:
- foo
- bar
```
## Example
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
```yaml ```yaml
locations: locations:
@ -16,8 +40,22 @@ locations:
- bar - bar
``` ```
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command. ## Global Options
> For flags without arguments you can set them to `true`. They will be handled accordingly. It is possible to specify global flags that will be run every time restic is invoked. To do so specify them under `global` in your config file.
```yaml
global:
all:
cache-dir: ~/restic
backup:
tag:
- foo
backends:
# ...
locations:
# ...
```
> :ToCPrevNext > :ToCPrevNext

View File

@ -3,7 +3,7 @@
## Installation ## Installation
```bash ```bash
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
``` ```
See [installation](/installation) for alternative options. See [installation](/installation) for alternative options.

View File

@ -31,7 +31,7 @@ else
fi fi
echo $ARCH echo $ARCH
curl -s https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \ wget -qO - https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
| grep "browser_download_url.*_${OS}_${ARCH}" \ | grep "browser_download_url.*_${OS}_${ARCH}" \
| cut -d : -f 2,3 \ | cut -d : -f 2,3 \
| tr -d \" \ | tr -d \" \

View File

@ -58,20 +58,27 @@ func (b Backend) generateRepo() (string, error) {
func (b Backend) getEnv() (map[string]string, error) { func (b Backend) getEnv() (map[string]string, error) {
env := make(map[string]string) env := make(map[string]string)
// Key
if b.Key != "" { if b.Key != "" {
env["RESTIC_PASSWORD"] = b.Key env["RESTIC_PASSWORD"] = b.Key
} else {
key, err := b.getKey()
if err != nil {
return nil, err
}
env["RESTIC_PASSWORD"] = key
} }
// From config file
repo, err := b.generateRepo() repo, err := b.generateRepo()
env["RESTIC_REPOSITORY"] = repo env["RESTIC_REPOSITORY"] = repo
for key, value := range b.Env { for key, value := range b.Env {
env[strings.ToUpper(key)] = value env[strings.ToUpper(key)] = value
} }
// From Envfile and passed as env
var prefix = "AUTORESTIC_" + strings.ToUpper(b.name) + "_"
for _, variable := range os.Environ() {
var splitted = strings.SplitN(variable, "=", 2)
if strings.HasPrefix(splitted[0], prefix) {
env[strings.TrimPrefix(splitted[0], prefix)] = splitted[1]
}
}
return env, err return env, err
} }
@ -85,17 +92,6 @@ func generateRandomKey() string {
return key return key
} }
func (b Backend) getKey() (string, error) {
if b.Key != "" {
return b.Key, nil
}
keyName := "AUTORESTIC_" + strings.ToUpper(b.name) + "_KEY"
if key, found := os.LookupEnv(keyName); found {
return key, nil
}
return "", fmt.Errorf("no key found for backend \"%s\"", b.name)
}
func (b Backend) validate() error { func (b Backend) validate() error {
if b.Type == "" { if b.Type == "" {
return fmt.Errorf(`Backend "%s" has no "type"`, b.name) return fmt.Errorf(`Backend "%s" has no "type"`, b.name)
@ -105,8 +101,9 @@ func (b Backend) validate() error {
} }
if b.Key == "" { if b.Key == "" {
// Check if key is set in environment // Check if key is set in environment
if _, err := b.getKey(); err != nil { env, _ := b.getEnv()
// If not generate a new one if _, found := env["RESTIC_PASSWORD"]; !found {
// No key set in config file or env => generate random key and save file
key := generateRandomKey() key := generateRandomKey()
b.Key = key b.Key = key
c := GetConfig() c := GetConfig()

View File

@ -47,11 +47,11 @@ func dlJSON(url string) (GithubRelease, error) {
func Uninstall(restic bool) error { func Uninstall(restic bool) error {
if err := os.Remove(path.Join(INSTALL_PATH, "autorestic")); err != nil { if err := os.Remove(path.Join(INSTALL_PATH, "autorestic")); err != nil {
colors.Error.Println(err) return err
} }
if restic { if restic {
if err := os.Remove(path.Join(INSTALL_PATH, "restic")); err != nil { if err := os.Remove(path.Join(INSTALL_PATH, "restic")); err != nil {
colors.Error.Println(err) return err
} }
} }
return nil return nil
@ -79,11 +79,15 @@ func downloadAndInstallAsset(body GithubRelease, name string) error {
return err return err
} }
defer tmp.Close() defer tmp.Close()
tmp.Chmod(0755) if err := tmp.Chmod(0755); err != nil {
io.Copy(tmp, bz) return err
}
if _, err := io.Copy(tmp, bz); err != nil {
return err
}
to := path.Join(INSTALL_PATH, name) to := path.Join(INSTALL_PATH, name)
os.Remove(to) // Delete if current, ignore error if file does not exits. defer os.Remove(to) // Delete if current, ignore error if file does not exits.
if err := os.Rename(tmp.Name(), to); err != nil { if err := os.Rename(tmp.Name(), to); err != nil {
return nil return nil
} }
@ -121,9 +125,11 @@ func Upgrade(restic bool) error {
// Upgrade restic // Upgrade restic
if restic { if restic {
if err := InstallRestic(); err != nil { if err := InstallRestic(); err != nil {
colors.Error.Println(err) return err
}
if err := upgradeRestic(); err != nil {
return err
} }
upgradeRestic()
} }
// Upgrade self // Upgrade self
@ -140,7 +146,9 @@ func Upgrade(restic bool) error {
return err return err
} }
if current.LT(latest) { if current.LT(latest) {
downloadAndInstallAsset(body, "autorestic") if err := downloadAndInstallAsset(body, "autorestic"); err != nil {
return err
}
colors.Success.Println("Updated autorestic") colors.Success.Println("Updated autorestic")
} else { } else {
colors.Body.Println("Already up to date") colors.Body.Println("Already up to date")

View File

@ -16,16 +16,20 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const VERSION = "1.3.0" const VERSION = "1.4.0"
var CI bool = false var CI bool = false
var VERBOSE bool = false var VERBOSE bool = false
var CRON_LEAN bool = false var CRON_LEAN bool = false
type OptionMap map[string][]interface{}
type Options map[string]OptionMap
type Config struct { type Config struct {
Extras interface{} `yaml:"extras"` Extras interface{} `yaml:"extras"`
Locations map[string]Location `yaml:"locations"` Locations map[string]Location `yaml:"locations"`
Backends map[string]Backend `yaml:"backends"` Backends map[string]Backend `yaml:"backends"`
Global Options `yaml:"global"`
} }
var once sync.Once var once sync.Once
@ -185,20 +189,18 @@ func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) {
selected, _ = cmd.Flags().GetStringSlice("location") selected, _ = cmd.Flags().GetStringSlice("location")
} }
for _, s := range selected { for _, s := range selected {
found := false var splitted = strings.Split(s, "@")
for _, l := range list { for _, l := range list {
if l == s { if l == splitted[0] {
found = true goto found
break
} }
} }
if !found { if backends {
if backends { return nil, fmt.Errorf("invalid backend \"%s\"", s)
return nil, fmt.Errorf("invalid backend \"%s\"", s) } else {
} else { return nil, fmt.Errorf("invalid location \"%s\"", s)
return nil, fmt.Errorf("invalid location \"%s\"", s)
}
} }
found:
} }
if len(selected) == 0 { if len(selected) == 0 {
@ -235,23 +237,40 @@ func (c *Config) SaveConfig() error {
return viper.WriteConfig() return viper.WriteConfig()
} }
func getOptions(options Options, key string) []string { func optionToString(option string) string {
var selected []string if !strings.HasPrefix(option, "-") {
for k, values := range options[key] { return "--" + option
}
return option
}
func appendOptionsToSlice(str *[]string, options OptionMap) {
for key, values := range options {
for _, value := range values { for _, value := range values {
// Bool // Bool
asBool, ok := value.(bool) asBool, ok := value.(bool)
if ok && asBool { if ok && asBool {
selected = append(selected, fmt.Sprintf("--%s", k)) *str = append(*str, optionToString(key))
continue continue
} }
// String // String
asString, ok := value.(string) asString, ok := value.(string)
if ok { if ok {
selected = append(selected, fmt.Sprintf("--%s", k), asString) *str = append(*str, optionToString(key), asString)
continue continue
} }
} }
} }
}
func getOptions(options Options, key string) []string {
var selected []string
var keys = []string{"all"}
if key != "" {
keys = append(keys, key)
}
for _, key := range keys {
appendOptionsToSlice(&selected, options[key])
}
return selected return selected
} }

View File

@ -31,8 +31,6 @@ type Hooks struct {
Failure HookArray `yaml:"failure,omitempty"` Failure HookArray `yaml:"failure,omitempty"`
} }
type Options map[string]map[string][]interface{}
type Location struct { type Location struct {
name string `yaml:",omitempty"` name string `yaml:",omitempty"`
From string `yaml:"from,omitempty"` From string `yaml:"from,omitempty"`
@ -125,8 +123,9 @@ func (l Location) getPath() (string, error) {
return "", fmt.Errorf("could not get path for location \"%s\"", l.name) return "", fmt.Errorf("could not get path for location \"%s\"", l.name)
} }
func (l Location) Backup(cron bool) []error { func (l Location) Backup(cron bool, specificBackend string) []error {
var errors []error var errors []error
var backends []string
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name) colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
t := l.getType() t := l.getType()
options := ExecuteOptions{ options := ExecuteOptions{
@ -155,7 +154,17 @@ func (l Location) Backup(cron bool) []error {
} }
// Backup // Backup
for i, to := range l.To { if specificBackend == "" {
backends = l.To
} else {
if l.hasBackend(specificBackend) {
backends = []string{specificBackend}
} else {
errors = append(errors, fmt.Errorf("backup location \"%s\" has no backend \"%s\"", l.name, specificBackend))
return errors
}
}
for i, to := range backends {
backend, _ := GetBackend(to) backend, _ := GetBackend(to)
colors.Secondary.Printf("Backend: %s\n", backend.name) colors.Secondary.Printf("Backend: %s\n", backend.name)
env, err := backend.getEnv() env, err := backend.getEnv()
@ -338,7 +347,7 @@ func (l Location) RunCron() error {
now := time.Now() now := time.Now()
if now.After(next) { if now.After(next) {
lock.SetCron(l.name, now.Unix()) lock.SetCron(l.name, now.Unix())
l.Backup(true) l.Backup(true, "")
} else { } else {
if !CRON_LEAN { if !CRON_LEAN {
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name) colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)

View File

@ -53,6 +53,9 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) { func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) {
options.Command = RESTIC_BIN options.Command = RESTIC_BIN
var c = GetConfig()
var optionsAsString = getOptions(c.Global, "")
args = append(optionsAsString, args...)
return ExecuteCommand(options, args...) return ExecuteCommand(options, args...)
} }