Compare commits

..

1 Commits

Author SHA1 Message Date
9d1b9c68fd WIP: Automatically set restore target to / if all backup paths are absolute
Some checks failed
continuous-integration/drone/push Build is failing
2023-10-25 20:01:37 -07:00
17 changed files with 100 additions and 372 deletions

View File

@ -4,7 +4,7 @@ name: test
steps:
- name: test
image: golang:1.21
image: golang:1.20
environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands:
@ -13,7 +13,7 @@ steps:
- make test
- name: check
image: iamthefij/drone-pre-commit@sha256:30fa17489b86d7a4c3ad9c3ce2e152c25d82b8671e5609d322c6cae0baed89cd
image: iamthefij/drone-pre-commit:personal
---
kind: pipeline
@ -32,7 +32,7 @@ trigger:
steps:
- name: build all binaries
image: golang:1.21
image: golang:1.17
environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands:
@ -65,7 +65,7 @@ steps:
- name: push images
image: thegeeklab/drone-docker-buildx
settings:
repo: iamthefij/restic-scheduler
repo: iamthefij/resticscheduler
auto_tag: true
platforms:
- linux/amd64

View File

@ -31,8 +31,10 @@ linters:
- gocognit
- goconst
- gocritic
# - gocyclo # Using cyclop
- godot
- gofumpt
# - goerr113 # Using errorlint
- gofmt
- goheader
- goimports
- gomnd
@ -42,6 +44,7 @@ linters:
- gosec
- grouper
- importas
# - ireturn
- lll
- maintidx
- makezero
@ -56,9 +59,11 @@ linters:
- paralleltest
- prealloc
- predeclared
# - promlinter # Not common enough
- revive
- rowserrcheck
- sqlclosecheck
# - stylecheck # Using revive
- tagliatelle
- tenv
- testpackage
@ -66,11 +71,27 @@ linters:
- tparallel
- unconvert
- unparam
- varnamelen
- wastedassign
- whitespace
- wrapcheck
- wsl
disable:
- gochecknoglobals
- godox
- forbidigo
# Deprecated
- golint
- interfacer
- maligned
- scopelint
- ifshort
- varcheck
- structcheck
- deadcode
- exhaustivestruct
linters-settings:
gomnd:
settings:

View File

@ -1,7 +1,7 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v3.4.0
hooks:
- id: check-added-large-files
- id: check-yaml
@ -11,8 +11,10 @@ repos:
- id: end-of-file-fixer
- id: check-merge-conflict
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
rev: v0.4.0
hooks:
- id: go-fmt
- id: go-imports
- id: golangci-lint
args:
- --timeout=3m

View File

@ -1,23 +1,21 @@
FROM alpine:3.18
FROM alpine:3.17
RUN apk add --no-cache \
bash~=5 \
consul~=1 \
mariadb-client~=10 \
mariadb-connector-c~=3 \
nomad~=1 \
postgresql15-client~=15 \
rclone~=1.62 \
redis~=7 \
restic~=0.15 \
consul~=1.14 \
mariadb-client~=10.6 \
mariadb-connector-c~=3.3 \
nomad~=1.4 \
postgresql15-client~=15.3 \
rclone~=1.60 \
redis~=7.0 \
restic~=0.14 \
sqlite~=3 \
tzdata~=2024 \
tzdata~=2023c \
;
ARG TARGETOS
ARG TARGETARCH
COPY ./dist/restic-scheduler-$TARGETOS-$TARGETARCH /bin/restic-scheduler
COPY ./dist/resticscheduler-$TARGETOS-$TARGETARCH /bin/resticscheduler
HEALTHCHECK CMD ["wget", "-O", "-", "http://localhost:8080/health"]
ENTRYPOINT [ "/bin/restic-scheduler" ]
ENTRYPOINT [ "/bin/resticscheduler" ]

View File

@ -1,10 +1,11 @@
APP_NAME = restic-scheduler
APP_NAME = resticscheduler
VERSION ?= $(shell git describe --tags --dirty)
GOFILES = *.go
# Multi-arch targets are generated from this
TARGET_ALIAS = $(APP_NAME)-linux-amd64 $(APP_NAME)-linux-arm $(APP_NAME)-linux-arm64
TARGETS = $(addprefix dist/,$(TARGET_ALIAS))
CURRENT_GOARCH = $(shell go env GOARCH)
.QUOTE = "
CURRENT_GOARCH = $(shell go env | awk -F "=" '/GOARCH/ { gsub(/$(.QUOTE)/,"", $$2); print $$2}')
# Default make target will run tests
.DEFAULT_GOAL = test
@ -54,7 +55,7 @@ clean:
## Multi-arch targets
$(TARGETS): $(GOFILES)
mkdir -p ./dist
GOOS=$(word 3, $(subst -, ,$(@))) GOARCH=$(word 4, $(subst -, ,$(@))) CGO_ENABLED=0 \
GOOS=$(word 2, $(subst -, ,$(@))) GOARCH=$(word 3, $(subst -, ,$(@))) CGO_ENABLED=0 \
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
-o $@

210
README.md
View File

@ -1,211 +1,3 @@
# [restic-scheduler](/iamthefij/restic-scheduler)
## About
`restic-scheduler` is a tool designed to allow declarative scheduling of restic backups using HCL (HashiCorp Configuration Language). This tool simplifies the process of managing and automating backups by defining jobs in a configuration file.
## Getting Started
### Installation
You can install `restic-scheduler` using the following command:
```sh
go install git.iamthefij.com/iamthefij/restic-scheduler@latest
```
You can also download the latest release from the [releases page](https://git.iamthefij.com/iamthefij/restic-scheduler/releases).
Finally, if you prefer to use Docker, you can run something like the following command:
```sh
docker run -v /path/to/config:/config -v /path/to/data:/data iamthefij/restic-scheduler -config /config/jobs.hcl
```
### Prerequisites
If you're not using Docker, you'll need to ensure that `restic` is installed and available in your system's PATH. You can download and install restic from [here](https://restic.net/).
## Usage
### Command Line Interface
The `restic-scheduler` command line interface provides several options for managing backup, restore, and unlock jobs. Below are some examples of how to use this tool.
#### Display Version
To display the version of `restic-scheduler`, use the `-version` flag:
```sh
restic-scheduler -version
```
#### Run Backup Jobs
To run backup jobs, use the `-backup` flag followed by a comma-separated list of job names. Use `all` to run all backup jobs:
```sh
restic-scheduler -backup job1,job2
```
#### Run Restore Jobs
To run restore jobs, use the `-restore` flag followed by a comma-separated list of job names. Use `all` to run all restore jobs:
```sh
restic-scheduler -restore job1,job2
```
#### Unlock Job Repositories
To unlock job repositories, use the `-unlock` flag followed by a comma-separated list of job names. Use `all` to unlock all job repositories:
```sh
restic-scheduler -unlock job1,job2
```
#### Run Jobs Once and Exit
To run specified backup and restore jobs once and exit, use the `-once` flag:
```sh
restic-scheduler -backup job1 -restore job2 -once
```
#### Health Check and metrics API
To bind the health check and Prometheus metrics API to a specific address, use the `-addr` flag:
```sh
restic-scheduler -addr 0.0.0.0:8080
```
#### Metrics Push Gateway
To specify the URL of a Prometheus push gateway service for batch runs, use the `-push-gateway` flag:
```sh
restic-scheduler -push-gateway http://example.com
```
## HCL Configuration
The configuration for `restic-scheduler` is defined using HCL. Below is a description and example of how to define a backup job in the configuration file.
### Job Configuration
A job in the configuration file is defined using the `job` block. Each job must have a unique name, a schedule, and a configuration for restic. Additionally, tasks can be defined to perform specific actions before and after the backup.
#### Fields
- `name`: The name of the job.
- `schedule`: The cron schedule for the job.
- `config`: The restic configuration block.
- `repo`: The restic repository.
- `passphrase`: (Optional) The passphrase for the repository.
- `env`: (Optional) Environment variables for restic.
- `options`: (Optional) Global options for restic. See the `restic` command for details.
- `task`: (Optional) A list of tasks to run before and after the backup.
- `mysql`, `postgres`, `sqlite`: (Optional) Database-specific tasks.
- `backup`: The backup configuration block.
- `forget`: (Optional) Options for forgetting old snapshots.
### Example
Below is an example of a job configuration in HCL:
```hcl
// Example job file
job "MyApp" {
schedule = "* * * * *"
config {
repo = "s3://..."
passphrase = "foo"
# Some alternate ways to pass the passphrase to restic
# passphrase = env("RESTIC_PASSWORD")
# passphrase = readfile("/path/to/passphrase")
env = {
"foo" = "bar",
}
options {
VerboseLevel = 3
# Another alternate way to pass the passphrase to restic
# PasswordFile = "/path/to/passphrase"
}
}
mysql "DumpMainDB" {
hostname = "foo"
username = "bar"
dump_to = "/data/main.sql"
}
sqlite "DumpSqlite" {
path = "/db/sqlite.db"
dump_to = "/data/sqlite.db.bak"
}
task "Create biz file" {
pre_script {
on_backup = <<EOF
echo bar >> /biz.txt
EOF
}
post_script {
on_backup = <<EOF
rm /biz.txt
EOF
}
}
task "Run restore shell script" {
pre_script {
on_restore = "/foo/bar.sh"
}
}
backup {
files =[
"/data",
"/biz.txt",
]
backup_opts {
Tags = ["service"]
}
restore_opts {
Verify = true
# Since paths are absolute, restore to root
Target = "/"
}
}
forget {
KeepLast = 3
KeepWeekly = 2
KeepMonthly = 2
KeepYearly = 2
Prune = true
}
}
```
```sh
restic-scheduler jobs.hcl
```
This will read the job definitions from `jobs.hcl` and execute the specified jobs.
For more examples, check out `./config.hcl` or some of the example integration test configs in `./test/`.
## Contributing
Contributions are welcome! Please open an issue or submit a pull request on the [GitHub repository](https://git.iamthefij.com/iamthefij/restic-scheduler).
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
Job scheduler for Restic backups

View File

@ -15,7 +15,7 @@ services:
POSTGRES_DB: main
bootstrap:
image: restic-scheduler
image: resticscheduler
entrypoint: /bootstrap-tests.sh
environment:
MYSQL_HOST: mysql
@ -29,7 +29,7 @@ services:
- ./data:/data
main:
image: restic-scheduler
image: resticscheduler
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
@ -43,7 +43,7 @@ services:
- ./test-backup.hcl:/test-backup.hcl
validate:
image: restic-scheduler
image: resticscheduler
entrypoint: /validate-tests.sh
environment:
MYSQL_HOST: mysql

2
job.go
View File

@ -280,11 +280,9 @@ func (j Job) Run() {
result.LastError = err
} else {
Metrics.SnapshotCurrentCount.WithLabelValues(j.Name).Set(float64(len(snapshots)))
if len(snapshots) > 0 {
latestSnapshot := snapshots[len(snapshots)-1]
Metrics.SnapshotLatestTime.WithLabelValues(j.Name).Set(float64(latestSnapshot.Time.Unix()))
}
}
if result.Success {
Metrics.JobFailureCount.WithLabelValues(j.Name).Set(0.0)

View File

@ -148,9 +148,7 @@ func TestJobValidation(t *testing.T) {
Name: "Test job",
Schedule: "@daily",
Config: ValidResticConfig(),
Tasks: []main.JobTask{
{}, //nolint:exhaustruct
},
Tasks: []main.JobTask{{}},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{},
@ -168,9 +166,7 @@ func TestJobValidation(t *testing.T) {
Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{
{}, //nolint:exhaustruct
},
MySQL: []main.JobTaskMySQL{{}},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
},
@ -187,9 +183,7 @@ func TestJobValidation(t *testing.T) {
Forget: nil,
MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{
{}, //nolint:exhaustruct
},
Sqlite: []main.JobTaskSqlite{{}},
},
expectedErr: main.ErrMissingField,
},
@ -228,7 +222,6 @@ func TestConfigValidation(t *testing.T) {
Config: ValidResticConfig(),
Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
@ -246,7 +239,6 @@ func TestConfigValidation(t *testing.T) {
Config: nil,
Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
@ -294,8 +286,7 @@ func TestConfigValidation(t *testing.T) {
MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
}},
},
}}},
expectedErr: main.ErrMissingField,
},
}

42
main.go
View File

@ -31,10 +31,6 @@ func ParseConfig(path string) ([]Job, error) {
Params: []function.Parameter{{
Name: "var",
Type: cty.String,
AllowNull: false,
AllowUnknown: false,
AllowDynamicType: false,
AllowMarked: false,
}},
VarParam: nil,
Type: function.StaticReturnType(cty.String),
@ -46,10 +42,6 @@ func ParseConfig(path string) ([]Job, error) {
Params: []function.Parameter{{
Name: "path",
Type: cty.String,
AllowNull: false,
AllowUnknown: false,
AllowDynamicType: false,
AllowMarked: false,
}},
VarParam: nil,
Type: function.StaticReturnType(cty.String),
@ -189,32 +181,10 @@ 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
@ -226,7 +196,6 @@ 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)")
@ -237,12 +206,7 @@ func readFlags() Flags {
return flags
}
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)
}
func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, snapshot string) error {
// Run specified backup jobs
if err := runBackupJobs(jobs, backupJobs); err != nil {
return fmt.Errorf("Failed running backup jobs: %w", err)
@ -258,8 +222,6 @@ func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, unlockJobs, snapshot
func maybePushMetrics(metricsPushGateway string) error {
if metricsPushGateway != "" {
fmt.Println("Pushing metrics to push gateway")
if err := Metrics.PushToGateway(metricsPushGateway); err != nil {
return fmt.Errorf("Failed pushing metrics after jobs run: %w", err)
}
@ -291,7 +253,7 @@ func main() {
log.Fatalf("Failed to read jobs from files: %v", err)
}
if err := runSpecifiedJobs(jobs, flags.backup, flags.restore, flags.unlock, flags.restoreSnapshot); err != nil {
if err := runSpecifiedJobs(jobs, flags.backup, flags.restore, flags.restoreSnapshot); err != nil {
log.Fatal(err)
}

View File

@ -31,6 +31,7 @@ func TestReadJobs(t *testing.T) {
t.Parallel()
jobs, err := main.ReadJobs([]string{"./test/sample.hcl"})
if err != nil {
t.Errorf("Unexpected error reading jobs: %v", err)
}

View File

@ -19,6 +19,7 @@ func (m ResticMetrics) PushToGateway(url string) error {
err := push.New(url, "batch").
Gatherer(m.Registry).
Add()
if err != nil {
return fmt.Errorf("error pushing to registry %s: %w", url, err)
}

View File

@ -11,10 +11,18 @@ import (
"time"
)
var (
ErrRestic = errors.New("restic error")
ErrRepoNotFound = errors.Join(errors.New("repository not found or uninitialized"), ErrRestic)
)
var ErrRestic = errors.New("restic error")
var ErrRepoNotFound = errors.New("repository not found or uninitialized")
func lineIn(needle string, haystack []string) bool {
for _, line := range haystack {
if line == needle {
return true
}
}
return false
}
func maybeAddArgString(args []string, name, value string) []string {
if value != "" {
@ -48,41 +56,22 @@ func maybeAddArgsList(args []string, name string, value []string) []string {
return args
}
// CommandOptions interface dictates a ToArgs() method should return each commandline arg as a string slice.
type CommandOptions interface {
// ToArgs returns the structs arguments as a slice of strings.
ToArgs() []string
}
// GenericOpts allows passing an arbitrary string slice as a set of command line options compatible with CommandOptions.
type GenericOpts []string
// ToArgs returns the structs arguments as a slice of strings.
func (o GenericOpts) ToArgs() []string {
return o
}
// NoOpts is a struct that fulfils the CommandOptions interface but provides no arguments.
type NoOpts struct{}
// ToArgs returns the structs arguments as a slice of strings.
func (NoOpts) ToArgs() []string {
return []string{}
}
// UnlockOpts holds optional arguments for unlock command.
type UnlockOpts struct {
RemoveAll bool `hcl:"RemoveAll,optional"`
}
// ToArgs returns the structs arguments as a slice of strings.
func (uo UnlockOpts) ToArgs() (args []string) {
args = maybeAddArgBool(args, "--remove-all", uo.RemoveAll)
return
}
// BackupOpts holds optional arguments for the Restic backup command.
type BackupOpts struct {
Exclude []string `hcl:"Exclude,optional"`
Include []string `hcl:"Include,optional"`
@ -90,7 +79,6 @@ type BackupOpts struct {
Host string `hcl:"Host,optional"`
}
// ToArgs returns the structs arguments as a slice of strings.
func (bo BackupOpts) ToArgs() (args []string) {
args = maybeAddArgsList(args, "--exclude", bo.Exclude)
args = maybeAddArgsList(args, "--include", bo.Include)
@ -110,7 +98,6 @@ type RestoreOpts struct {
Verify bool `hcl:"Verify,optional"`
}
// ToArgs returns the structs arguments as a slice of strings.
func (ro RestoreOpts) ToArgs() (args []string) {
args = maybeAddArgsList(args, "--exclude", ro.Exclude)
args = maybeAddArgsList(args, "--include", ro.Include)
@ -150,7 +137,6 @@ type ForgetOpts struct {
Prune bool `hcl:"Prune,optional"`
}
// ToArgs returns the structs arguments as a slice of strings.
func (fo ForgetOpts) ToArgs() (args []string) {
args = maybeAddArgInt(args, "--keep-last", fo.KeepLast)
args = maybeAddArgInt(args, "--keep-hourly", fo.KeepHourly)
@ -207,15 +193,13 @@ type ResticGlobalOpts struct {
TLSClientCertFile string `hcl:"TlsClientCertFile,optional"`
LimitDownload int `hcl:"LimitDownload,optional"`
LimitUpload int `hcl:"LimitUpload,optional"`
VerboseLevel int `hcl:"VerboseLevel,optional"`
Options map[string]string `hcl:"Options,optional"`
VerboseLevel int `hcl:"VerboseLevel,optional"`
CleanupCache bool `hcl:"CleanupCache,optional"`
InsecureTLS bool `hcl:"InsecureTls,optional"`
NoCache bool `hcl:"NoCache,optional"`
NoLock bool `hcl:"NoLock,optional"`
}
// ToArgs returns the structs arguments as a slice of strings.
func (glo ResticGlobalOpts) ToArgs() (args []string) {
args = maybeAddArgString(args, "--cacert", glo.CaCertFile)
args = maybeAddArgString(args, "--cache-dir", glo.CacheDir)
@ -225,7 +209,6 @@ func (glo ResticGlobalOpts) ToArgs() (args []string) {
args = maybeAddArgInt(args, "--limit-upload", glo.LimitUpload)
args = maybeAddArgInt(args, "--verbose", glo.VerboseLevel)
args = maybeAddArgBool(args, "--cleanup-cache", glo.CleanupCache)
args = maybeAddArgBool(args, "--insecure-tls", glo.InsecureTLS)
args = maybeAddArgBool(args, "--no-cache", glo.NoCache)
args = maybeAddArgBool(args, "--no-lock", glo.NoLock)
@ -318,7 +301,7 @@ func (rcmd Restic) RunRestic(
responseErr = ErrRepoNotFound
}
return output, NewResticError(command, output.AllLines(), errors.Join(err, responseErr))
return output, NewResticError(command, output.AllLines(), responseErr)
}
return output, nil
@ -348,12 +331,6 @@ 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"`

View File

@ -32,7 +32,6 @@ func TestGlobalOptions(t *testing.T) {
LimitUpload: 1,
VerboseLevel: 1,
CleanupCache: true,
InsecureTLS: true,
NoCache: true,
NoLock: true,
Options: map[string]string{
@ -49,7 +48,6 @@ func TestGlobalOptions(t *testing.T) {
"--limit-upload", "1",
"--verbose", "1",
"--cleanup-cache",
"--insecure-tls",
"--no-cache",
"--no-lock",
"--option", "key='a long value'",
@ -152,20 +150,6 @@ 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()
@ -237,7 +221,7 @@ func TestResticInterface(t *testing.T) {
}
// Write test file to the data dir
err := os.WriteFile(dataFile, []byte("testing"), 0o644)
err := os.WriteFile(dataFile, []byte("testing"), 0644)
AssertEqualFail(t, "unexpected error writing to test file", nil, err)
// Make sure no existing repo is found
@ -297,7 +281,7 @@ func TestResticInterface(t *testing.T) {
AssertEqualFail(t, "unexpected error checking repo", nil, err)
// Change the data file
err = os.WriteFile(dataFile, []byte("unexpected"), 0o644)
err = os.WriteFile(dataFile, []byte("unexpected"), 0644)
AssertEqualFail(t, "unexpected error writing to test file", nil, err)
// Check that data wrote
@ -313,8 +297,4 @@ 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)
}

View File

@ -13,10 +13,8 @@ import (
"github.com/robfig/cron/v3"
)
var (
jobResultsLock = sync.Mutex{}
jobResults = map[string]JobResult{}
)
var jobResultsLock = sync.Mutex{}
var jobResults = map[string]JobResult{}
type JobResult struct {
JobName string
@ -115,8 +113,6 @@ func ScheduleAndRunJobs(jobs []Job) error {
defer func() {
ctx := scheduler.Stop()
<-ctx.Done()
fmt.Println("All jobs successfully stopped")
}()
return nil

View File

@ -6,6 +6,7 @@ import (
"io/fs"
"log"
"os"
"path"
"strings"
)
@ -405,6 +406,23 @@ func (t BackupFilesTask) RunRestore(cfg TaskConfig) error {
t.RestoreOpts = &RestoreOpts{} //nolint:exhaustruct
}
// If all backup paths are absolute and target is empty, use root as the restore target
if t.RestoreOpts.Target == "" {
allAbs := true
for _, backupPath := range t.Paths {
if !path.IsAbs(backupPath) {
allAbs = false
break
}
}
if allAbs {
t.RestoreOpts.Target = "/"
}
}
if t.snapshot == "" {
t.snapshot = "latest"
}

View File

@ -2,16 +2,6 @@ package main
import "fmt"
func lineIn(needle string, haystack []string) bool {
for _, line := range haystack {
if line == needle {
return true
}
}
return false
}
func MergeEnvMap(parent, child map[string]string) map[string]string {
result := map[string]string{}