mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2024-12-23 00:36:25 +00:00
430 lines
11 KiB
Go
430 lines
11 KiB
Go
package internal
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/cupcakearmy/autorestic/internal/colors"
|
|
"github.com/cupcakearmy/autorestic/internal/flags"
|
|
"github.com/cupcakearmy/autorestic/internal/lock"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/api/types/volume"
|
|
"github.com/docker/docker/client"
|
|
"github.com/joho/godotenv"
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const VERSION = "1.7.4"
|
|
|
|
type OptionMap map[string][]interface{}
|
|
type Options map[string]OptionMap
|
|
|
|
type Config struct {
|
|
Version string `mapstructure:"version"`
|
|
Extras interface{} `mapstructure:"extras"`
|
|
Locations map[string]Location `mapstructure:"locations"`
|
|
Backends map[string]Backend `mapstructure:"backends"`
|
|
Global Options `mapstructure:"global"`
|
|
}
|
|
|
|
var once sync.Once
|
|
var config *Config
|
|
|
|
func exitConfig(err error, msg string) {
|
|
if err != nil {
|
|
colors.Error.Println(err)
|
|
}
|
|
if msg != "" {
|
|
colors.Error.Println(msg)
|
|
}
|
|
lock.Unlock()
|
|
os.Exit(1)
|
|
}
|
|
|
|
func GetDockerVolumesLocations() (map[string]Location, error) {
|
|
var (
|
|
cli *client.Client
|
|
err error
|
|
volumeListOKBody volume.VolumeListOKBody
|
|
ctx context.Context
|
|
)
|
|
|
|
volumesList := make(map[string]Location)
|
|
|
|
if flags.DOCKER_HOST != "" {
|
|
cli, err = client.NewClientWithOpts(client.WithHost(flags.DOCKER_HOST))
|
|
} else {
|
|
cli, err = client.NewClientWithOpts()
|
|
}
|
|
if err != nil {
|
|
return volumesList, fmt.Errorf("Could not create client: %s", err.Error())
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx = context.Background()
|
|
volumeListOKBody, err = cli.VolumeList(ctx, filters.NewArgs())
|
|
if err != nil {
|
|
return volumesList, fmt.Errorf("Could not list volumes: %s", err.Error())
|
|
}
|
|
|
|
for _, volume := range volumeListOKBody.Volumes {
|
|
val, ok := volume.Labels["autorestic.enable"]
|
|
if ok && (val == "1" || val == "true") {
|
|
volumeTo, ok := volume.Labels["autorestic.to"]
|
|
if !ok {
|
|
return volumesList, fmt.Errorf("Autorestic is enabled but there is no \"autorestic.to\" label for volume %s", volume.Name)
|
|
}
|
|
volumeLocation := Location{
|
|
name: volume.Name,
|
|
From: []string{volume.Name},
|
|
Type: "volume",
|
|
To: strings.Split(volumeTo, ";"),
|
|
}
|
|
for label, value := range volume.Labels {
|
|
if label == "autorestic.cron" {
|
|
volumeLocation.Cron = value
|
|
} else if label == "autorestic.forget" {
|
|
volumeLocation.ForgetOption = LocationForgetOption(value)
|
|
} else if label == "autorestic.hooks.dir" {
|
|
volumeLocation.Hooks.Dir = value
|
|
} else if label == "autorestic.hooks.before" {
|
|
volumeLocation.Hooks.Before = []string{value}
|
|
} else if label == "autorestic.hooks.after" {
|
|
volumeLocation.Hooks.After = []string{value}
|
|
} else if label == "autorestic.hooks.success" {
|
|
volumeLocation.Hooks.Success = []string{value}
|
|
} else if label == "autorestic.hooks.failure" {
|
|
volumeLocation.Hooks.Failure = []string{value}
|
|
} else if strings.HasPrefix(label, "autorestic.options.") {
|
|
label = strings.TrimPrefix(label, "autorestic.options.")
|
|
target := strings.Split(label, ".")
|
|
if len(target) != 2 {
|
|
fmt.Errorf("Failed to parse label \"%s\" for volume %s", volume.Name)
|
|
}
|
|
|
|
values := strings.Split(value, ";")
|
|
interfaceValues := make([]interface{}, 0)
|
|
for _, v := range values {
|
|
interfaceValues = append(interfaceValues, interface{}(v))
|
|
}
|
|
|
|
if volumeLocation.Options == nil {
|
|
volumeLocation.Options = make(Options)
|
|
}
|
|
volumeLocation.Options[target[0]] = make(OptionMap)
|
|
volumeLocation.Options[target[0]][target[1]] = interfaceValues
|
|
} else if strings.HasPrefix(label, "autorestic.copy.") {
|
|
fromBackend := strings.TrimPrefix(label, "autorestic.copy.")
|
|
if volumeLocation.CopyOption == nil {
|
|
volumeLocation.CopyOption = make(LocationCopy)
|
|
}
|
|
volumeLocation.CopyOption[fromBackend] = strings.Split(value, ";")
|
|
}
|
|
}
|
|
volumesList[volume.Name] = volumeLocation
|
|
}
|
|
}
|
|
|
|
return volumesList, nil
|
|
}
|
|
|
|
func GetConfig() *Config {
|
|
if config == nil {
|
|
once.Do(func() {
|
|
if err := viper.ReadInConfig(); err == nil {
|
|
absConfig, _ := filepath.Abs(viper.ConfigFileUsed())
|
|
if !flags.CRON_LEAN {
|
|
colors.Faint.Println("Using config: \t", absConfig)
|
|
}
|
|
// Load env file
|
|
envFile := filepath.Join(filepath.Dir(absConfig), ".autorestic.env")
|
|
err = godotenv.Load(envFile)
|
|
if err == nil && !flags.CRON_LEAN {
|
|
colors.Faint.Println("Using env:\t", envFile)
|
|
}
|
|
} else {
|
|
text := err.Error()
|
|
if strings.Contains(text, "no such file or directory") {
|
|
cfgFileName := ".autorestic"
|
|
colors.Error.Println(
|
|
fmt.Sprintf(
|
|
"cannot find configuration file '%s.yml' or '%s.yaml'.",
|
|
cfgFileName, cfgFileName))
|
|
} else {
|
|
colors.Error.Println("could not load config file\n" + text)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
var versionConfig interface{}
|
|
viper.UnmarshalKey("version", &versionConfig)
|
|
if versionConfig == nil {
|
|
exitConfig(nil, "no version specified in config file. please see docs on how to migrate")
|
|
}
|
|
version, ok := versionConfig.(int)
|
|
if !ok {
|
|
exitConfig(nil, "version specified in config file is not an int")
|
|
} else {
|
|
// Check for version
|
|
if version != 2 {
|
|
exitConfig(nil, "unsupported config version number. please check the docs for migration\nhttps://autorestic.vercel.app/migration/")
|
|
}
|
|
}
|
|
|
|
config = &Config{}
|
|
if err := viper.UnmarshalExact(config); err != nil {
|
|
exitConfig(err, "Could not parse config file!")
|
|
}
|
|
|
|
if flags.DOCKER_DISCOVERY {
|
|
if config.Locations == nil {
|
|
config.Locations = make(map[string]Location)
|
|
}
|
|
volumeLocations, err := GetDockerVolumesLocations()
|
|
if err != nil {
|
|
exitConfig(err, "failed to load locations from docker")
|
|
}
|
|
for name, location := range volumeLocations {
|
|
config.Locations[name] = location
|
|
}
|
|
}
|
|
})
|
|
}
|
|
return config
|
|
}
|
|
|
|
func GetPathRelativeToConfig(p string) (string, error) {
|
|
if path.IsAbs(p) {
|
|
return p, nil
|
|
} else if strings.HasPrefix(p, "~") {
|
|
home, err := homedir.Dir()
|
|
return path.Join(home, strings.TrimPrefix(p, "~")), err
|
|
} else {
|
|
return path.Join(path.Dir(viper.ConfigFileUsed()), p), nil
|
|
}
|
|
}
|
|
|
|
func (c *Config) Describe() {
|
|
// Locations
|
|
for name, l := range c.Locations {
|
|
var tmp string
|
|
colors.PrimaryPrint(`Location: "%s"`, name)
|
|
|
|
tmp = ""
|
|
for _, path := range l.From {
|
|
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("←"), path)
|
|
}
|
|
colors.PrintDescription("From", tmp)
|
|
|
|
tmp = ""
|
|
for _, to := range l.To {
|
|
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("→"), to)
|
|
}
|
|
colors.PrintDescription("To", tmp)
|
|
|
|
if l.Cron != "" {
|
|
colors.PrintDescription("Cron", l.Cron)
|
|
}
|
|
|
|
tmp = ""
|
|
hooks := map[string][]string{
|
|
"Before": l.Hooks.Before,
|
|
"After": l.Hooks.After,
|
|
"Failure": l.Hooks.Failure,
|
|
"Success": l.Hooks.Success,
|
|
}
|
|
for hook, commands := range hooks {
|
|
if len(commands) > 0 {
|
|
tmp += "\n\t" + hook
|
|
for _, cmd := range commands {
|
|
tmp += colors.Faint.Sprintf("\n\t ▶ %s", cmd)
|
|
}
|
|
}
|
|
}
|
|
if tmp != "" {
|
|
colors.PrintDescription("Hooks", tmp)
|
|
}
|
|
|
|
if len(l.Options) > 0 {
|
|
tmp = ""
|
|
for t, options := range l.Options {
|
|
tmp += "\n\t" + t
|
|
for option, values := range options {
|
|
for _, value := range values {
|
|
tmp += colors.Faint.Sprintf("\n\t ✧ --%s=%s", option, value)
|
|
}
|
|
}
|
|
}
|
|
colors.PrintDescription("Options", tmp)
|
|
}
|
|
}
|
|
|
|
// Backends
|
|
for name, b := range c.Backends {
|
|
colors.PrimaryPrint("Backend: \"%s\"", name)
|
|
colors.PrintDescription("Type", b.Type)
|
|
colors.PrintDescription("Path", b.Path)
|
|
|
|
if len(b.Env) > 0 {
|
|
tmp := ""
|
|
for option, value := range b.Env {
|
|
tmp += fmt.Sprintf("\n\t%s %s %s", colors.Success.Sprint("✧"), strings.ToUpper(option), colors.Faint.Sprint(value))
|
|
}
|
|
colors.PrintDescription("Env", tmp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func CheckConfig() error {
|
|
c := GetConfig()
|
|
if c == nil {
|
|
return fmt.Errorf("config could not be loaded/found")
|
|
}
|
|
if !CheckIfResticIsCallable() {
|
|
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, flags.RESTIC_BIN)
|
|
}
|
|
for name, backend := range c.Backends {
|
|
backend.name = name
|
|
if err := backend.validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for name, location := range c.Locations {
|
|
location.name = name
|
|
if err := location.validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) {
|
|
var list []string
|
|
if backends {
|
|
for name := range config.Backends {
|
|
list = append(list, name)
|
|
}
|
|
} else {
|
|
for name := range config.Locations {
|
|
list = append(list, name)
|
|
}
|
|
}
|
|
|
|
all, _ := cmd.Flags().GetBool("all")
|
|
if all {
|
|
return list, nil
|
|
}
|
|
|
|
var selected []string
|
|
if backends {
|
|
selected, _ = cmd.Flags().GetStringSlice("backend")
|
|
} else {
|
|
selected, _ = cmd.Flags().GetStringSlice("location")
|
|
}
|
|
for _, s := range selected {
|
|
var splitted = strings.Split(s, "@")
|
|
for _, l := range list {
|
|
if l == splitted[0] {
|
|
goto found
|
|
}
|
|
}
|
|
if backends {
|
|
return nil, fmt.Errorf("invalid backend \"%s\"", s)
|
|
} else {
|
|
return nil, fmt.Errorf("invalid location \"%s\"", s)
|
|
}
|
|
found:
|
|
}
|
|
|
|
if len(selected) == 0 {
|
|
return selected, fmt.Errorf("nothing selected, aborting")
|
|
}
|
|
return selected, nil
|
|
}
|
|
|
|
func AddFlagsToCommand(cmd *cobra.Command, backend bool) {
|
|
var usage string
|
|
if backend {
|
|
usage = "all backends"
|
|
} else {
|
|
usage = "all locations"
|
|
}
|
|
cmd.PersistentFlags().BoolP("all", "a", false, usage)
|
|
if backend {
|
|
cmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "select backends")
|
|
} else {
|
|
cmd.PersistentFlags().StringSliceP("location", "l", []string{}, "select locations")
|
|
}
|
|
}
|
|
|
|
func (c *Config) SaveConfig() error {
|
|
file := viper.ConfigFileUsed()
|
|
if err := CopyFile(file, file+".old"); err != nil {
|
|
return err
|
|
}
|
|
colors.Secondary.Println("Saved a backup copy of your file next to the original.")
|
|
|
|
viper.Set("backends", c.Backends)
|
|
viper.Set("locations", c.Locations)
|
|
|
|
return viper.WriteConfig()
|
|
}
|
|
|
|
func optionToString(option string) string {
|
|
if !strings.HasPrefix(option, "-") {
|
|
return "--" + option
|
|
}
|
|
return option
|
|
}
|
|
|
|
func appendOptionsToSlice(str *[]string, options OptionMap) {
|
|
for key, values := range options {
|
|
for _, value := range values {
|
|
// Bool
|
|
asBool, ok := value.(bool)
|
|
if ok && asBool {
|
|
*str = append(*str, optionToString(key))
|
|
continue
|
|
}
|
|
*str = append(*str, optionToString(key), fmt.Sprint(value))
|
|
}
|
|
}
|
|
}
|
|
|
|
func getOptions(options Options, keys []string) []string {
|
|
var selected []string
|
|
for _, key := range keys {
|
|
appendOptionsToSlice(&selected, options[key])
|
|
}
|
|
return selected
|
|
}
|
|
|
|
func combineBackendOptions(key string, b Backend) []string {
|
|
// Priority: backend > global
|
|
var options []string
|
|
gFlags := getOptions(GetConfig().Global, []string{key})
|
|
bFlags := getOptions(b.Options, []string{"all", key})
|
|
options = append(options, gFlags...)
|
|
options = append(options, bFlags...)
|
|
return options
|
|
}
|
|
|
|
func combineAllOptions(key string, l Location, b Backend) []string {
|
|
// Priority: location > backend > global
|
|
var options []string
|
|
gFlags := getOptions(GetConfig().Global, []string{key})
|
|
bFlags := getOptions(b.Options, []string{"all", key})
|
|
lFlags := getOptions(l.Options, []string{"all", key})
|
|
options = append(options, gFlags...)
|
|
options = append(options, bFlags...)
|
|
options = append(options, lFlags...)
|
|
return options
|
|
}
|