Compare commits

..

2 Commits

Author SHA1 Message Date
f9f00951a7 Fix bin path for docker
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-04-13 15:13:29 -07:00
3d590997bc WIP: Begin work on metrics
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-13 13:44:48 -07:00
26 changed files with 298 additions and 1356 deletions

View File

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

5
.gitignore vendored
View File

@ -20,9 +20,4 @@ dist/
# Built executable # Built executable
restic-scheduler restic-scheduler
resticscheduler
data/ data/
# Itest temp dirs
itest/data
itest/repo

View File

@ -1,13 +1,16 @@
--- ---
linters: linters:
enable: enable:
- deadcode
- errcheck - errcheck
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unused - unused
- varcheck
- asciicheck - asciicheck
- bidichk - bidichk
@ -16,13 +19,14 @@ linters:
- contextcheck - contextcheck
- cyclop - cyclop
- decorder - decorder
- depguard
- dupl - dupl
- durationcheck - durationcheck
- errchkjson - errchkjson
- errname - errname
- errorlint - errorlint
- exhaustive - exhaustive
- exhaustruct - exhaustivestruct
- exportloopref - exportloopref
- forcetypeassert - forcetypeassert
- funlen - funlen
@ -31,8 +35,10 @@ linters:
- gocognit - gocognit
- goconst - goconst
- gocritic - gocritic
# - gocyclo # Using cyclop
- godot - godot
- gofumpt # - goerr113 # Using errorlint
- gofmt
- goheader - goheader
- goimports - goimports
- gomnd - gomnd
@ -41,7 +47,9 @@ linters:
- goprintffuncname - goprintffuncname
- gosec - gosec
- grouper - grouper
- ifshort
- importas - importas
# - ireturn
- lll - lll
- maintidx - maintidx
- makezero - makezero
@ -56,9 +64,11 @@ linters:
- paralleltest - paralleltest
- prealloc - prealloc
- predeclared - predeclared
# - promlinter # Not common enough
- revive - revive
- rowserrcheck - rowserrcheck
- sqlclosecheck - sqlclosecheck
# - stylecheck # Using revive
- tagliatelle - tagliatelle
- tenv - tenv
- testpackage - testpackage
@ -66,22 +76,37 @@ linters:
- tparallel - tparallel
- unconvert - unconvert
- unparam - unparam
- varnamelen
- wastedassign - wastedassign
- whitespace - whitespace
- wrapcheck - wrapcheck
- wsl - wsl
disable:
- gochecknoglobals
- godox
- forbidigo
# Deprecated
- golint
- interfacer
- maligned
- scopelint
linters-settings: linters-settings:
# gosec:
# excludes:
# - G204
gomnd: gomnd:
settings: settings:
mnd: mnd:
ignored-functions: math.* ignored-functions: math.*
issues: issues:
fix: true
exclude-rules: exclude-rules:
- path: _test\.go - path: _test\.go
linters: linters:
- errcheck - errcheck
- gosec - gosec
- funlen - funlen
# Enable autofix
fix: true

View File

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

View File

@ -1,23 +1,7 @@
FROM alpine:3.18 FROM scratch
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 \
sqlite~=3 \
tzdata~=2024 \
;
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH 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/resticscheduler" ]
ENTRYPOINT [ "/bin/restic-scheduler" ]

View File

@ -1,11 +1,10 @@
APP_NAME = restic-scheduler APP_NAME = resticscheduler
VERSION ?= $(shell git describe --tags --dirty) VERSION ?= $(shell git describe --tags --dirty)
GOFILES = *.go GOFILES = *.go
# Multi-arch targets are generated from this # Multi-arch targets are generated from this
TARGET_ALIAS = $(APP_NAME)-linux-amd64 $(APP_NAME)-linux-arm $(APP_NAME)-linux-arm64 TARGET_ALIAS = $(APP_NAME)-linux-amd64 $(APP_NAME)-linux-arm $(APP_NAME)-linux-arm64 $(APP_NAME)-darwin-amd64 $(APP_NAME)-darwin-arm64
TARGETS = $(addprefix dist/,$(TARGET_ALIAS)) TARGETS = $(addprefix dist/,$(TARGET_ALIAS))
CURRENT_GOARCH = $(shell go env GOARCH) #
# Default make target will run tests # Default make target will run tests
.DEFAULT_GOAL = test .DEFAULT_GOAL = test
@ -28,13 +27,9 @@ build: $(APP_NAME)
# Run all tests # Run all tests
.PHONY: test .PHONY: test
test: test:
go test -v -coverprofile=coverage.out # -short go test -coverprofile=coverage.out # -short
go tool cover -func=coverage.out go tool cover -func=coverage.out
.PHONY: itest
itest: docker-build
./itest/run.sh
# Installs pre-commit hooks # Installs pre-commit hooks
.PHONY: install-hooks .PHONY: install-hooks
install-hooks: install-hooks:
@ -54,7 +49,7 @@ clean:
## Multi-arch targets ## Multi-arch targets
$(TARGETS): $(GOFILES) $(TARGETS): $(GOFILES)
mkdir -p ./dist 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 \ go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
-o $@ -o $@
@ -62,5 +57,5 @@ $(TARGETS): $(GOFILES)
$(TARGET_ALIAS): $(TARGET_ALIAS):
$(MAKE) $(addprefix dist/,$@) $(MAKE) $(addprefix dist/,$@)
docker-build: dist/$(APP_NAME)-linux-$(CURRENT_GOARCH) # $(addprefix docker-,$(TARGET_ALIAS)):
docker build --platform=linux/$(CURRENT_GOARCH) . -t $(APP_NAME) # docker build --build-arg BIN=dist/$(@:docker-%=%) .

210
README.md
View File

@ -1,211 +1,3 @@
# [restic-scheduler](/iamthefij/restic-scheduler) # [restic-scheduler](/iamthefij/restic-scheduler)
## About Job scheduler for Restic backups
`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.

18
go.mod
View File

@ -1,11 +1,11 @@
module git.iamthefij.com/iamthefij/restic-scheduler module git.iamthefij.com/iamthefij/restic-scheduler
go 1.20 go 1.17
require ( require (
github.com/go-test/deep v1.0.8 github.com/go-test/deep v1.0.8
github.com/hashicorp/hcl/v2 v2.11.1 github.com/hashicorp/hcl/v2 v2.11.1
github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_golang v1.12.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/zclconf/go-cty v1.8.0 github.com/zclconf/go-cty v1.8.0
) )
@ -16,13 +16,13 @@ require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-cmp v0.5.5 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.6 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.26.0 // indirect
) )

34
go.sum
View File

@ -71,11 +71,9 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
@ -120,9 +118,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -180,28 +177,24 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -298,15 +291,12 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -316,7 +306,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -353,22 +342,17 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -415,6 +399,7 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -490,9 +475,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -1,46 +0,0 @@
#! /bin/sh
set -ex
# Create flat file
echo "Hello" > /data/test.txt
# Create Sqlite database
touch /data/test_database.db
sqlite3 /data/test_database.db <<-EOF
CREATE TABLE test_table (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL
);
INSERT INTO test_table(data)
VALUES ("Test row");
EOF
# Create MySql database
until mysql --host "$MYSQL_HOST" --user "$MYSQL_USER" --password="$MYSQL_PWD" --execute "SHOW DATABASES;"; do
sleep 1
done
mysql --host "$MYSQL_HOST" --user "$MYSQL_USER" --password="$MYSQL_PWD" main <<EOF
CREATE TABLE test_table (
id INTEGER AUTO_INCREMENT PRIMARY KEY,
data TEXT NOT NULL
);
INSERT INTO test_table(data)
VALUES ("Test row");
EOF
# Create Postgres database
export PGPASSWORD="$PGSQL_PASS"
until psql --host "$PGSQL_HOST" --username "$PGSQL_USER" --command "SELECT datname FROM pg_database;"; do
sleep 1
done
psql -v ON_ERROR_STOP=1 --host "$PGSQL_HOST" --username "$PGSQL_USER" main <<EOF
CREATE TABLE test_table (
id SERIAL PRIMARY KEY,
data TEXT NOT NULL
);
INSERT INTO test_table(data)
VALUES ('Test row');
EOF

View File

@ -1,57 +0,0 @@
---
version: "3.9"
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: shhh
MYSQL_DATABASE: main
postgres:
image: postgres
environment:
POSTGRES_PASSWORD: shhh
POSTGRES_DB: main
bootstrap:
image: restic-scheduler
entrypoint: /bootstrap-tests.sh
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PWD: shhh
PGSQL_HOST: postgres
PGSQL_USER: postgres
PGSQL_PASS: shhh
volumes:
- ./bootstrap-tests.sh:/bootstrap-tests.sh
- ./data:/data
main:
image: restic-scheduler
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PWD: shhh
PGSQL_HOST: postgres
PGSQL_USER: postgres
PGSQL_PASS: shhh
volumes:
- ./repo:/repo
- ./data:/data
- ./test-backup.hcl:/test-backup.hcl
validate:
image: restic-scheduler
entrypoint: /validate-tests.sh
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PWD: shhh
PGSQL_HOST: postgres
PGSQL_USER: postgres
PGSQL_PASS: shhh
volumes:
- ./validate-tests.sh:/validate-tests.sh
- ./data:/data

View File

@ -1,35 +0,0 @@
#! /bin/bash
set -ex
cd "$(dirname "$0")"
mkdir -p ./repo ./data
echo Clean everything
docker-compose down -v
rm -fr ./repo/* ./data/*
sleep 5
echo Boostrap databases and data
docker-compose up -d mysql postgres
docker-compose run bootstrap
sleep 1
echo Run backup job
docker-compose run main -backup IntegrationTest -once /test-backup.hcl
echo Clean data
docker-compose down -v
docker-compose up -d mysql postgres
rm -fr ./data/*
sleep 15
echo Run restore
docker-compose run main -restore IntegrationTest -once /test-backup.hcl
sleep 1
echo Validate data
docker-compose run validate
echo Clean all again
docker-compose down -v
rm -fr ./repo/* ./data/*

View File

@ -1,38 +0,0 @@
job "IntegrationTest" {
schedule = "@daily"
config {
repo = "/repo"
passphrase = "shh"
}
mysql "MySQL" {
hostname = env("MYSQL_HOST")
database = "main"
username = env("MYSQL_USER")
password = env("MYSQL_PWD")
dump_to = "/tmp/mysql.sql"
}
postgres "Postgres" {
hostname = env("PGSQL_HOST")
database = "main"
username = env("PGSQL_USER")
password = env("PGSQL_PASS")
create = true
dump_to = "/tmp/psql.sql"
}
sqlite "SQLite" {
path = "/data/test_database.db"
dump_to = "/data/test_database.db.bak"
}
backup {
paths = ["/data"]
restore_opts {
Target = "/"
}
}
}

View File

@ -1,21 +0,0 @@
#! /bin/sh
set -ex
# Check flat file
test -f /data/test.txt
grep "^Hello" /data/test.txt
# Check Sqlite database
test -f /data/test_database.db
sqlite3 /data/test_database.db "select data from test_table where id = 1" | grep "^Test row"
# Check MySql database
mysql --host "$MYSQL_HOST" --user "$MYSQL_USER" --password="$MYSQL_PWD" main <<EOF | grep "^Test row"
select data from test_table where id = 1;
EOF
# Check Postgres database
export PGPASSWORD="$PGSQL_PASS"
psql --host "$PGSQL_HOST" --user "$PGSQL_USER" main <<EOF | grep "Test row"
select data from test_table where id = 1;
EOF

93
job.go
View File

@ -52,21 +52,19 @@ func (r ResticConfig) Validate() error {
type Job struct { type Job struct {
Name string `hcl:"name,label"` Name string `hcl:"name,label"`
Schedule string `hcl:"schedule"` Schedule string `hcl:"schedule"`
Config *ResticConfig `hcl:"config,block"` Config ResticConfig `hcl:"config,block"`
Tasks []JobTask `hcl:"task,block"` Tasks []JobTask `hcl:"task,block"`
Backup BackupFilesTask `hcl:"backup,block"` Backup BackupFilesTask `hcl:"backup,block"`
Forget *ForgetOpts `hcl:"forget,block"` Forget *ForgetOpts `hcl:"forget,block"`
// Meta Tasks // Meta Tasks
// NOTE: Now that these are also available within a task MySQL []JobTaskMySQL `hcl:"mysql,block"`
// these could be removed to make task order more obvious Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
MySQL []JobTaskMySQL `hcl:"mysql,block"`
Postgres []JobTaskPostgres `hcl:"postgres,block"`
Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
// Metrics and health // Metrics and health
healthy bool healthy bool
lastErr error lastErr error
metrics ResticMetrics
} }
func (j Job) validateTasks() error { func (j Job) validateTasks() error {
@ -76,24 +74,6 @@ func (j Job) validateTasks() error {
} }
} }
for _, mysql := range j.MySQL {
if err := mysql.Validate(); err != nil {
return fmt.Errorf("job %s has an invalid task: %w", j.Name, err)
}
}
for _, pg := range j.Postgres {
if err := pg.Validate(); err != nil {
return fmt.Errorf("job %s has an invalid task: %w", j.Name, err)
}
}
for _, sqlite := range j.Sqlite {
if err := sqlite.Validate(); err != nil {
return fmt.Errorf("job %s has an invalid task: %w", j.Name, err)
}
}
return nil return nil
} }
@ -103,11 +83,7 @@ func (j Job) Validate() error {
} }
if _, err := cron.ParseStandard(j.Schedule); err != nil { if _, err := cron.ParseStandard(j.Schedule); err != nil {
return fmt.Errorf("job %s has an invalid schedule: %w: %w", j.Name, err, ErrInvalidConfigValue) return fmt.Errorf("job %s has an invalid schedule: %v: %w", j.Name, err, ErrInvalidConfigValue)
}
if j.Config == nil {
return fmt.Errorf("job %s is missing restic config: %w", j.Name, ErrMissingField)
} }
if err := j.Config.Validate(); err != nil { if err := j.Config.Validate(); err != nil {
@ -118,6 +94,18 @@ func (j Job) Validate() error {
return err return err
} }
for _, mysql := range j.MySQL {
if err := mysql.Validate(); err != nil {
return fmt.Errorf("job %s has an invalid task: %w", j.Name, err)
}
}
for _, sqlite := range j.Sqlite {
if err := sqlite.Validate(); err != nil {
return fmt.Errorf("job %s has an invalid task: %w", j.Name, err)
}
}
if err := j.Backup.Validate(); err != nil { if err := j.Backup.Validate(); err != nil {
return fmt.Errorf("job %s has an invalid backup config: %w", j.Name, err) return fmt.Errorf("job %s has an invalid backup config: %w", j.Name, err)
} }
@ -133,10 +121,6 @@ func (j Job) AllTasks() []ExecutableTask {
allTasks = append(allTasks, mysql.GetPreTask()) allTasks = append(allTasks, mysql.GetPreTask())
} }
for _, pg := range j.Postgres {
allTasks = append(allTasks, pg.GetPreTask())
}
for _, sqlite := range j.Sqlite { for _, sqlite := range j.Sqlite {
allTasks = append(allTasks, sqlite.GetPreTask()) allTasks = append(allTasks, sqlite.GetPreTask())
} }
@ -157,10 +141,6 @@ func (j Job) AllTasks() []ExecutableTask {
allTasks = append(allTasks, mysql.GetPostTask()) allTasks = append(allTasks, mysql.GetPostTask())
} }
for _, pg := range j.Postgres {
allTasks = append(allTasks, pg.GetPostTask())
}
for _, sqlite := range j.Sqlite { for _, sqlite := range j.Sqlite {
allTasks = append(allTasks, sqlite.GetPostTask()) allTasks = append(allTasks, sqlite.GetPostTask())
} }
@ -175,10 +155,6 @@ func (j Job) BackupPaths() []string {
paths = append(paths, t.DumpToPath) paths = append(paths, t.DumpToPath)
} }
for _, t := range j.Postgres {
paths = append(paths, t.DumpToPath)
}
for _, t := range j.Sqlite { for _, t := range j.Sqlite {
paths = append(paths, t.DumpToPath) paths = append(paths, t.DumpToPath)
} }
@ -222,7 +198,7 @@ func (j Job) Logger() *log.Logger {
return GetLogger(j.Name) return GetLogger(j.Name)
} }
func (j Job) RunRestore(snapshot string) error { func (j Job) RunRestore() error {
logger := j.Logger() logger := j.Logger()
restic := j.NewRestic() restic := j.NewRestic()
@ -238,10 +214,6 @@ func (j Job) RunRestore(snapshot string) error {
Env: nil, Env: nil,
} }
if backupTask, ok := exTask.(BackupFilesTask); ok {
backupTask.snapshot = snapshot
}
if err := exTask.RunRestore(taskCfg); err != nil { if err := exTask.RunRestore(taskCfg); err != nil {
return fmt.Errorf("failed running job %s: %w", j.Name, err) return fmt.Errorf("failed running job %s: %w", j.Name, err)
} }
@ -263,8 +235,6 @@ func (j Job) Run() {
Message: "", Message: "",
} }
Metrics.JobStartTime.WithLabelValues(j.Name).SetToCurrentTime()
if err := j.RunBackup(); err != nil { if err := j.RunBackup(); err != nil {
j.healthy = false j.healthy = false
j.lastErr = err j.lastErr = err
@ -275,23 +245,6 @@ func (j Job) Run() {
result.LastError = err result.LastError = err
} }
snapshots, err := j.NewRestic().ReadSnapshots()
if err != nil {
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)
} else {
Metrics.JobFailureCount.WithLabelValues(j.Name).Inc()
}
JobComplete(result) JobComplete(result)
} }
@ -307,8 +260,8 @@ func (j Job) NewRestic() *Restic {
} }
type Config struct { type Config struct {
DefaultConfig *ResticConfig `hcl:"default_config,block"` // GlobalConfig *ResticConfig `hcl:"global_config,block"`
Jobs []Job `hcl:"job,block"` Jobs []Job `hcl:"job,block"`
} }
func (c Config) Validate() error { func (c Config) Validate() error {
@ -317,12 +270,6 @@ func (c Config) Validate() error {
} }
for _, job := range c.Jobs { for _, job := range c.Jobs {
// Use default restic config if no job config is provided
// TODO: Maybe merge values here
if job.Config == nil {
job.Config = c.DefaultConfig
}
if err := job.Validate(); err != nil { if err := job.Validate(); err != nil {
return err return err
} }

View File

@ -7,8 +7,8 @@ import (
main "git.iamthefij.com/iamthefij/restic-scheduler" main "git.iamthefij.com/iamthefij/restic-scheduler"
) )
func ValidResticConfig() *main.ResticConfig { func ValidResticConfig() main.ResticConfig {
return &main.ResticConfig{ return main.ResticConfig{
Passphrase: "shh", Passphrase: "shh",
Repo: "./data", Repo: "./data",
Env: nil, Env: nil,
@ -27,12 +27,12 @@ func TestResticConfigValidate(t *testing.T) {
{ {
name: "missing passphrase", name: "missing passphrase",
expectedErr: main.ErrMutuallyExclusive, expectedErr: main.ErrMutuallyExclusive,
config: main.ResticConfig{}, //nolint:exhaustruct config: main.ResticConfig{}, // nolint:exhaustivestruct
}, },
{ {
name: "passphrase no file", name: "passphrase no file",
expectedErr: nil, expectedErr: nil,
//nolint:exhaustruct // nolint:exhaustivestruct
config: main.ResticConfig{ config: main.ResticConfig{
Passphrase: "shh", Passphrase: "shh",
}, },
@ -40,7 +40,7 @@ func TestResticConfigValidate(t *testing.T) {
{ {
name: "file no passphrase", name: "file no passphrase",
expectedErr: nil, expectedErr: nil,
//nolint:exhaustruct // nolint:exhaustivestruct
config: main.ResticConfig{ config: main.ResticConfig{
GlobalOpts: &main.ResticGlobalOpts{ GlobalOpts: &main.ResticGlobalOpts{
PasswordFile: "file", PasswordFile: "file",
@ -50,7 +50,7 @@ func TestResticConfigValidate(t *testing.T) {
{ {
name: "file and passphrase", name: "file and passphrase",
expectedErr: main.ErrMutuallyExclusive, expectedErr: main.ErrMutuallyExclusive,
//nolint:exhaustruct // nolint:exhaustivestruct
config: main.ResticConfig{ config: main.ResticConfig{
Passphrase: "shh", Passphrase: "shh",
GlobalOpts: &main.ResticGlobalOpts{ GlobalOpts: &main.ResticGlobalOpts{
@ -89,10 +89,9 @@ func TestJobValidation(t *testing.T) {
Schedule: "@daily", Schedule: "@daily",
Config: ValidResticConfig(), Config: ValidResticConfig(),
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
}, },
expectedErr: nil, expectedErr: nil,
@ -104,10 +103,9 @@ func TestJobValidation(t *testing.T) {
Schedule: "@daily", Schedule: "@daily",
Config: ValidResticConfig(), Config: ValidResticConfig(),
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
}, },
expectedErr: main.ErrMissingField, expectedErr: main.ErrMissingField,
@ -119,10 +117,9 @@ func TestJobValidation(t *testing.T) {
Schedule: "shrug", Schedule: "shrug",
Config: ValidResticConfig(), Config: ValidResticConfig(),
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
}, },
expectedErr: main.ErrInvalidConfigValue, expectedErr: main.ErrInvalidConfigValue,
@ -132,12 +129,11 @@ func TestJobValidation(t *testing.T) {
job: main.Job{ job: main.Job{
Name: "Test job", Name: "Test job",
Schedule: "@daily", Schedule: "@daily",
Config: &main.ResticConfig{}, //nolint:exhaustruct Config: main.ResticConfig{}, // nolint:exhaustivestruct
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
}, },
expectedErr: main.ErrMutuallyExclusive, expectedErr: main.ErrMutuallyExclusive,
@ -148,13 +144,10 @@ func TestJobValidation(t *testing.T) {
Name: "Test job", Name: "Test job",
Schedule: "@daily", Schedule: "@daily",
Config: ValidResticConfig(), Config: ValidResticConfig(),
Tasks: []main.JobTask{ Tasks: []main.JobTask{{}},
{}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
}, },
expectedErr: main.ErrMissingField, expectedErr: main.ErrMissingField,
@ -166,12 +159,9 @@ func TestJobValidation(t *testing.T) {
Schedule: "@daily", Schedule: "@daily",
Config: ValidResticConfig(), Config: ValidResticConfig(),
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{ MySQL: []main.JobTaskMySQL{{}},
{}, //nolint:exhaustruct
},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
}, },
expectedErr: main.ErrMissingField, expectedErr: main.ErrMissingField,
@ -183,13 +173,10 @@ func TestJobValidation(t *testing.T) {
Schedule: "@daily", Schedule: "@daily",
Config: ValidResticConfig(), Config: ValidResticConfig(),
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{}, Sqlite: []main.JobTaskSqlite{{}},
Sqlite: []main.JobTaskSqlite{
{}, //nolint:exhaustruct
},
}, },
expectedErr: main.ErrMissingField, expectedErr: main.ErrMissingField,
}, },
@ -220,82 +207,34 @@ func TestConfigValidation(t *testing.T) {
}{ }{
{ {
name: "Valid job", name: "Valid job",
config: main.Config{ config: main.Config{Jobs: []main.Job{{
DefaultConfig: nil, Name: "Valid job",
Jobs: []main.Job{{ Schedule: "@daily",
Name: "Valid job", Config: ValidResticConfig(),
Schedule: "@daily", Tasks: []main.JobTask{},
Config: ValidResticConfig(), Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
Tasks: []main.JobTask{}, MySQL: []main.JobTaskMySQL{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Sqlite: []main.JobTaskSqlite{},
Forget: nil, }}},
MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
}},
},
expectedErr: nil, expectedErr: nil,
}, },
{ {
name: "Valid job with default config", name: "No jobs",
config: main.Config{ config: main.Config{Jobs: []main.Job{}},
DefaultConfig: ValidResticConfig(),
Jobs: []main.Job{{
Name: "Valid job",
Schedule: "@daily",
Config: nil,
Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
}},
},
expectedErr: nil,
},
{
name: "No jobs",
config: main.Config{
DefaultConfig: nil,
Jobs: []main.Job{},
},
expectedErr: main.ErrNoJobsFound, expectedErr: main.ErrNoJobsFound,
}, },
{ {
name: "Invalid name", name: "Invalid name",
config: main.Config{ config: main.Config{Jobs: []main.Job{{
DefaultConfig: nil, Name: "",
Jobs: []main.Job{{ Schedule: "@daily",
Name: "", Config: ValidResticConfig(),
Schedule: "@daily", Tasks: []main.JobTask{},
Config: ValidResticConfig(), Backup: main.BackupFilesTask{Paths: []string{"/test"}}, // nolint:exhaustivestruct
Tasks: []main.JobTask{}, Forget: nil,
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct MySQL: []main.JobTaskMySQL{},
Forget: nil, Sqlite: []main.JobTaskSqlite{},
MySQL: []main.JobTaskMySQL{}, }}},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
}},
},
expectedErr: main.ErrMissingField,
},
{
name: "Missing config",
config: main.Config{
DefaultConfig: nil,
Jobs: []main.Job{{
Name: "",
Schedule: "@daily",
Config: nil,
Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
}},
},
expectedErr: main.ErrMissingField, expectedErr: main.ErrMissingField,
}, },
} }

211
main.go
View File

@ -1,12 +1,10 @@
package main package main
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/exec"
"strings" "strings"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
@ -17,8 +15,7 @@ import (
var ( var (
// version of restic-scheduler being run. // version of restic-scheduler being run.
version = "dev" version = "dev"
ErrJobNotFound = errors.New("jobs not found")
) )
func ParseConfig(path string) ([]Job, error) { func ParseConfig(path string) ([]Job, error) {
@ -29,12 +26,8 @@ func ParseConfig(path string) ([]Job, error) {
Functions: map[string]function.Function{ Functions: map[string]function.Function{
"env": function.New(&function.Spec{ "env": function.New(&function.Spec{
Params: []function.Parameter{{ Params: []function.Parameter{{
Name: "var", Name: "var",
Type: cty.String, Type: cty.String,
AllowNull: false,
AllowUnknown: false,
AllowDynamicType: false,
AllowMarked: false,
}}, }},
VarParam: nil, VarParam: nil,
Type: function.StaticReturnType(cty.String), Type: function.StaticReturnType(cty.String),
@ -44,12 +37,8 @@ func ParseConfig(path string) ([]Job, error) {
}), }),
"readfile": function.New(&function.Spec{ "readfile": function.New(&function.Spec{
Params: []function.Parameter{{ Params: []function.Parameter{{
Name: "path", Name: "path",
Type: cty.String, Type: cty.String,
AllowNull: false,
AllowUnknown: false,
AllowDynamicType: false,
AllowMarked: false,
}}, }},
VarParam: nil, VarParam: nil,
Type: function.StaticReturnType(cty.String), Type: function.StaticReturnType(cty.String),
@ -98,21 +87,11 @@ func ReadJobs(paths []string) ([]Job, error) {
} }
} }
if len(allJobs) == 0 {
return allJobs, fmt.Errorf("No jobs found in provided configuration: %w", ErrJobNotFound)
}
return allJobs, nil return allJobs, nil
} }
type Set map[string]bool type Set map[string]bool
func (s Set) Contains(key string) bool {
_, contains := s[key]
return contains
}
func NewSetFrom(l []string) Set { func NewSetFrom(l []string) Set {
s := make(Set) s := make(Set)
for _, l := range l { for _, l := range l {
@ -122,146 +101,30 @@ func NewSetFrom(l []string) Set {
return s return s
} }
// FilterJobs filters a list of jobs by a list of names. func runBackupJobs(jobs []Job, names []string) error {
func FilterJobs(jobs []Job, names []string) ([]Job, error) {
nameSet := NewSetFrom(names) nameSet := NewSetFrom(names)
if nameSet.Contains("all") { _, runAll := nameSet["all"]
return jobs, nil
}
filteredJobs := []Job{}
for _, job := range jobs { for _, job := range jobs {
if nameSet.Contains(job.Name) { if _, found := nameSet[job.Name]; runAll || found {
filteredJobs = append(filteredJobs, job) if err := job.RunBackup(); err != nil {
return err
delete(nameSet, job.Name) }
} }
} }
var err error
if len(nameSet) > 0 {
err = fmt.Errorf("%w: %v", ErrJobNotFound, nameSet)
}
return filteredJobs, err
}
func runBackupJobs(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.RunBackup(); err != nil {
return err
}
}
return filterJobErr
}
func runRestoreJobs(jobs []Job, names string, snapshot 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.RunRestore(snapshot); err != nil {
return err
}
}
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
metricsPushGateway string
}
func readFlags() Flags {
flags := Flags{} //nolint:exhaustruct
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)")
flag.StringVar(&JobBaseDir, "base-dir", JobBaseDir, "Base dir to create intermediate job files like SQL dumps.")
flag.StringVar(&flags.restoreSnapshot, "snapshot", "latest", "the snapshot to restore")
flag.Parse()
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)
}
// Run specified backup jobs
if err := runBackupJobs(jobs, backupJobs); err != nil {
return fmt.Errorf("Failed running backup jobs: %w", err)
}
// Run specified restore jobs
if err := runRestoreJobs(jobs, restoreJobs, snapshot); err != nil {
return fmt.Errorf("Failed running restore jobs: %w", err)
}
return nil return nil
} }
func maybePushMetrics(metricsPushGateway string) error { func runRestoreJobs(jobs []Job, names []string) error {
if metricsPushGateway != "" { nameSet := NewSetFrom(names)
fmt.Println("Pushing metrics to push gateway") _, runAll := nameSet["all"]
if err := Metrics.PushToGateway(metricsPushGateway); err != nil { for _, job := range jobs {
return fmt.Errorf("Failed pushing metrics after jobs run: %w", err) if _, found := nameSet[job.Name]; runAll || found {
if err := job.RunRestore(); err != nil {
return err
}
} }
} }
@ -269,19 +132,21 @@ func maybePushMetrics(metricsPushGateway string) error {
} }
func main() { func main() {
flags := readFlags() showVersion := flag.Bool("version", false, "Display the version and exit")
backup := flag.String("backup", "", "Run backup jobs now. Names are comma separated and `all` will run all.")
restore := flag.String("restore", "", "Run restore jobs now. Names are comma separated and `all` will run all.")
once := flag.Bool("once", false, "Run jobs specified using -backup and -restore once and exit")
healthCheckAddr := flag.String("addr", "0.0.0.0:8080", "address to bind health check API")
flag.StringVar(&JobBaseDir, "base-dir", JobBaseDir, "Base dir to create intermediate job files like SQL dumps.")
flag.Parse()
// Print version if flag is provided // Print version if flag is provided
if flags.showVersion { if *showVersion {
fmt.Println("restic-scheduler version:", version) fmt.Println("restic-scheduler version:", version)
return return
} }
if _, err := exec.LookPath("restic"); err != nil {
log.Fatalf("Could not find restic in path. Make sure it's installed")
}
if flag.NArg() == 0 { if flag.NArg() == 0 {
log.Fatalf("Requires a path to a job file, but found none") log.Fatalf("Requires a path to a job file, but found none")
} }
@ -291,21 +156,27 @@ func main() {
log.Fatalf("Failed to read jobs from files: %v", err) 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 len(jobs) == 0 {
log.Fatal(err) log.Fatal("No jobs found in provided configuration")
}
// Run specified backup jobs
if err := runBackupJobs(jobs, strings.Split(*backup, ",")); err != nil {
log.Fatalf("Failed running backup jobs: %v", err)
}
// Run specified restore jobs
if err := runRestoreJobs(jobs, strings.Split(*restore, ",")); err != nil {
log.Fatalf("Failed running backup jobs: %v", err)
} }
// Exit if only running once // Exit if only running once
if flags.once { if *once {
if err := maybePushMetrics(flags.metricsPushGateway); err != nil {
log.Fatal(err)
}
return return
} }
go func() { go func() {
_ = RunHTTPHandlers(flags.healthCheckAddr) _ = RunHTTPHandlers(*healthCheckAddr)
}() }()
// TODO: Add healthcheck handler using Job.Healthy() // TODO: Add healthcheck handler using Job.Healthy()

View File

@ -1,10 +1,8 @@
package main_test package main_test
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"reflect"
"testing" "testing"
main "git.iamthefij.com/iamthefij/restic-scheduler" main "git.iamthefij.com/iamthefij/restic-scheduler"
@ -18,9 +16,9 @@ func TestMain(m *testing.M) {
if testResult == 0 && testing.CoverMode() != "" { if testResult == 0 && testing.CoverMode() != "" {
c := testing.Coverage() c := testing.Coverage()
if c < MinCoverage { if c < MinCoverage {
fmt.Printf("WARNING: Tests passed but coverage failed at %0.2f and minimum to pass is %0.2f\n", c, MinCoverage) fmt.Printf("Tests passed but coverage failed at %0.2f and minimum to pass is %0.2f\n", c, MinCoverage)
testResult = 0 testResult = -1
} }
} }
@ -31,6 +29,7 @@ func TestReadJobs(t *testing.T) {
t.Parallel() t.Parallel()
jobs, err := main.ReadJobs([]string{"./test/sample.hcl"}) jobs, err := main.ReadJobs([]string{"./test/sample.hcl"})
if err != nil { if err != nil {
t.Errorf("Unexpected error reading jobs: %v", err) t.Errorf("Unexpected error reading jobs: %v", err)
} }
@ -39,66 +38,3 @@ func TestReadJobs(t *testing.T) {
t.Error("Expected read jobs but found none") t.Error("Expected read jobs but found none")
} }
} }
func TestRunJobs(t *testing.T) {
t.Parallel()
validJob := main.Job{
Name: "Valid job",
Schedule: "@daily",
Config: ValidResticConfig(),
Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{},
}
cases := []struct {
name string
jobs []main.Job
names []string
expected []main.Job
expectedError error
}{
{
name: "Found job",
jobs: []main.Job{validJob},
names: []string{"Valid job"},
expected: []main.Job{validJob},
expectedError: nil,
},
{
name: "Run all",
jobs: []main.Job{validJob},
names: []string{"all"},
expected: []main.Job{validJob},
expectedError: nil,
},
{
name: "Extra, missing job",
jobs: []main.Job{validJob},
names: []string{"Valid job", "Not Found"},
expected: []main.Job{validJob},
expectedError: main.ErrJobNotFound,
},
}
for _, c := range cases {
testCase := c
t.Run(testCase.name+" backup", func(t *testing.T) {
t.Parallel()
jobs, err := main.FilterJobs(testCase.jobs, testCase.names)
if !reflect.DeepEqual(jobs, testCase.expected) {
t.Errorf("expected %v but found %v", testCase.expected, jobs)
}
if !errors.Is(err, testCase.expectedError) {
t.Errorf("expected %v but found %v", testCase.expectedError, err)
}
})
}
}

View File

@ -1,39 +1,24 @@
package main package main
import ( import (
"fmt"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/push"
) )
type ResticMetrics struct { type ResticMetrics struct {
JobStartTime *prometheus.GaugeVec JobRunTime *prometheus.GaugeVec
JobFailureCount *prometheus.GaugeVec JobRunDuration *prometheus.GaugeVec
SnapshotCurrentCount *prometheus.GaugeVec SnapshotCount *prometheus.GaugeVec
SnapshotLatestTime *prometheus.GaugeVec LatestSnapshotTime *prometheus.GaugeVec
Registry *prometheus.Registry LatestSnapshotSize *prometheus.GaugeVec
}
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)
}
return nil
} }
func InitMetrics() *ResticMetrics { func InitMetrics() *ResticMetrics {
labelNames := []string{"job"} labelNames := []string{"job"}
metrics := &ResticMetrics{ metrics := &ResticMetrics{
Registry: prometheus.NewRegistry(), JobRunTime: prometheus.NewGaugeVec(
JobStartTime: prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "restic_job_start_time", Name: "job_run_time",
Help: "time that a job was run", Help: "time that a job was run",
Namespace: "", Namespace: "",
Subsystem: "", Subsystem: "",
@ -41,29 +26,29 @@ func InitMetrics() *ResticMetrics {
}, },
labelNames, labelNames,
), ),
JobFailureCount: prometheus.NewGaugeVec( JobRunDuration: prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "restic_job_failure_count", Name: "job_run_duration",
Help: "number of consecutive failures for jobs", Help: "time it took for the last job run",
Namespace: "", Namespace: "",
Subsystem: "", Subsystem: "",
ConstLabels: nil, ConstLabels: nil,
}, },
labelNames, labelNames,
), ),
SnapshotCurrentCount: prometheus.NewGaugeVec( SnapshotCount: prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "restic_snapshot_current_total", Name: "snapshot_total",
Help: "number of current snapshots", Help: "time it took for the last job run",
Namespace: "", Namespace: "",
Subsystem: "", Subsystem: "",
ConstLabels: nil, ConstLabels: nil,
}, },
labelNames, labelNames,
), ),
SnapshotLatestTime: prometheus.NewGaugeVec( LatestSnapshotTime: prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "restic_snapshot_latest_time", Name: "latest_snapshot_time",
Help: "time of the most recent snapshot", Help: "time of the most recent snapshot",
Namespace: "", Namespace: "",
Subsystem: "", Subsystem: "",
@ -71,14 +56,29 @@ func InitMetrics() *ResticMetrics {
}, },
labelNames, labelNames,
), ),
LatestSnapshotSize: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "latest_snapshot_size",
Help: "size of the most recent snapshot",
Namespace: "",
Subsystem: "",
ConstLabels: nil,
},
labelNames,
),
} }
metrics.Registry.MustRegister(metrics.JobStartTime) prometheus.MustRegister(metrics.JobRunTime)
metrics.Registry.MustRegister(metrics.JobFailureCount) prometheus.MustRegister(metrics.JobRunDuration)
metrics.Registry.MustRegister(metrics.SnapshotCurrentCount) prometheus.MustRegister(metrics.SnapshotCount)
metrics.Registry.MustRegister(metrics.SnapshotLatestTime) prometheus.MustRegister(metrics.LatestSnapshotTime)
prometheus.MustRegister(metrics.LatestSnapshotSize)
return metrics return metrics
} }
var Metrics = InitMetrics() var Metrics = InitMetrics()
func JobComplete() {
}

123
restic.go
View File

@ -11,10 +11,18 @@ import (
"time" "time"
) )
var ( var ErrRestic = errors.New("restic error")
ErrRestic = errors.New("restic error") var ErrRepoNotFound = errors.New("repository not found or uninitialized")
ErrRepoNotFound = errors.Join(errors.New("repository not found or uninitialized"), ErrRestic)
) 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 { func maybeAddArgString(args []string, name, value string) []string {
if value != "" { if value != "" {
@ -48,41 +56,22 @@ func maybeAddArgsList(args []string, name string, value []string) []string {
return args return args
} }
// CommandOptions interface dictates a ToArgs() method should return each commandline arg as a string slice.
type CommandOptions interface { type CommandOptions interface {
// ToArgs returns the structs arguments as a slice of strings.
ToArgs() []string ToArgs() []string
} }
// GenericOpts allows passing an arbitrary string slice as a set of command line options compatible with CommandOptions.
type GenericOpts []string type GenericOpts []string
// ToArgs returns the structs arguments as a slice of strings.
func (o GenericOpts) ToArgs() []string { func (o GenericOpts) ToArgs() []string {
return o return o
} }
// NoOpts is a struct that fulfils the CommandOptions interface but provides no arguments.
type NoOpts struct{} type NoOpts struct{}
// ToArgs returns the structs arguments as a slice of strings.
func (NoOpts) ToArgs() []string { func (NoOpts) ToArgs() []string {
return []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 { type BackupOpts struct {
Exclude []string `hcl:"Exclude,optional"` Exclude []string `hcl:"Exclude,optional"`
Include []string `hcl:"Include,optional"` Include []string `hcl:"Include,optional"`
@ -90,7 +79,6 @@ type BackupOpts struct {
Host string `hcl:"Host,optional"` Host string `hcl:"Host,optional"`
} }
// ToArgs returns the structs arguments as a slice of strings.
func (bo BackupOpts) ToArgs() (args []string) { func (bo BackupOpts) ToArgs() (args []string) {
args = maybeAddArgsList(args, "--exclude", bo.Exclude) args = maybeAddArgsList(args, "--exclude", bo.Exclude)
args = maybeAddArgsList(args, "--include", bo.Include) args = maybeAddArgsList(args, "--include", bo.Include)
@ -110,7 +98,6 @@ type RestoreOpts struct {
Verify bool `hcl:"Verify,optional"` Verify bool `hcl:"Verify,optional"`
} }
// ToArgs returns the structs arguments as a slice of strings.
func (ro RestoreOpts) ToArgs() (args []string) { func (ro RestoreOpts) ToArgs() (args []string) {
args = maybeAddArgsList(args, "--exclude", ro.Exclude) args = maybeAddArgsList(args, "--exclude", ro.Exclude)
args = maybeAddArgsList(args, "--include", ro.Include) args = maybeAddArgsList(args, "--include", ro.Include)
@ -150,7 +137,6 @@ type ForgetOpts struct {
Prune bool `hcl:"Prune,optional"` Prune bool `hcl:"Prune,optional"`
} }
// ToArgs returns the structs arguments as a slice of strings.
func (fo ForgetOpts) ToArgs() (args []string) { func (fo ForgetOpts) ToArgs() (args []string) {
args = maybeAddArgInt(args, "--keep-last", fo.KeepLast) args = maybeAddArgInt(args, "--keep-last", fo.KeepLast)
args = maybeAddArgInt(args, "--keep-hourly", fo.KeepHourly) args = maybeAddArgInt(args, "--keep-hourly", fo.KeepHourly)
@ -201,21 +187,18 @@ func (fo ForgetOpts) ToArgs() (args []string) {
} }
type ResticGlobalOpts struct { type ResticGlobalOpts struct {
CaCertFile string `hcl:"CaCertFile,optional"` CaCertFile string `hcl:"CaCertFile,optional"`
CacheDir string `hcl:"CacheDir,optional"` CacheDir string `hcl:"CacheDir,optional"`
PasswordFile string `hcl:"PasswordFile,optional"` PasswordFile string `hcl:"PasswordFile,optional"`
TLSClientCertFile string `hcl:"TlsClientCertFile,optional"` TLSClientCertFile string `hcl:"TlsClientCertFile,optional"`
LimitDownload int `hcl:"LimitDownload,optional"` LimitDownload int `hcl:"LimitDownload,optional"`
LimitUpload int `hcl:"LimitUpload,optional"` LimitUpload int `hcl:"LimitUpload,optional"`
VerboseLevel int `hcl:"VerboseLevel,optional"` VerboseLevel int `hcl:"VerboseLevel,optional"`
Options map[string]string `hcl:"Options,optional"` CleanupCache bool `hcl:"CleanupCache,optional"`
CleanupCache bool `hcl:"CleanupCache,optional"` NoCache bool `hcl:"NoCache,optional"`
InsecureTLS bool `hcl:"InsecureTls,optional"` NoLock bool `hcl:"NoLock,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) { func (glo ResticGlobalOpts) ToArgs() (args []string) {
args = maybeAddArgString(args, "--cacert", glo.CaCertFile) args = maybeAddArgString(args, "--cacert", glo.CaCertFile)
args = maybeAddArgString(args, "--cache-dir", glo.CacheDir) args = maybeAddArgString(args, "--cache-dir", glo.CacheDir)
@ -225,14 +208,9 @@ func (glo ResticGlobalOpts) ToArgs() (args []string) {
args = maybeAddArgInt(args, "--limit-upload", glo.LimitUpload) args = maybeAddArgInt(args, "--limit-upload", glo.LimitUpload)
args = maybeAddArgInt(args, "--verbose", glo.VerboseLevel) args = maybeAddArgInt(args, "--verbose", glo.VerboseLevel)
args = maybeAddArgBool(args, "--cleanup-cache", glo.CleanupCache) 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-cache", glo.NoCache)
args = maybeAddArgBool(args, "--no-lock", glo.NoLock) args = maybeAddArgBool(args, "--no-lock", glo.NoLock)
for key, value := range glo.Options {
args = append(args, "--option", fmt.Sprintf("%s='%s'", key, value))
}
return args return args
} }
@ -290,11 +268,7 @@ func (e *ResticError) Unwrap() error {
return e.OriginalError return e.OriginalError
} }
func (rcmd Restic) RunRestic( func (rcmd Restic) RunRestic(command string, options CommandOptions, commandArgs ...string) ([]string, error) {
command string,
options CommandOptions,
commandArgs ...string,
) (*CapturedCommandLogWriter, error) {
args := []string{} args := []string{}
if rcmd.GlobalOpts != nil { if rcmd.GlobalOpts != nil {
args = rcmd.GlobalOpts.ToArgs() args = rcmd.GlobalOpts.ToArgs()
@ -306,22 +280,22 @@ func (rcmd Restic) RunRestic(
cmd := exec.Command("restic", args...) cmd := exec.Command("restic", args...)
output := NewCapturedCommandLogWriter(rcmd.Logger) output := NewCapturedLogWriter(rcmd.Logger)
cmd.Stdout = output.Stdout cmd.Stdout = output
cmd.Stderr = output.Stderr cmd.Stderr = output
cmd.Env = rcmd.BuildEnv() cmd.Env = rcmd.BuildEnv()
cmd.Dir = rcmd.Cwd cmd.Dir = rcmd.Cwd
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
responseErr := ErrRestic responseErr := ErrRestic
if lineIn("Is there a repository at the following location?", output.Stderr.Lines) { if lineIn("Is there a repository at the following location?", output.Lines) {
responseErr = ErrRepoNotFound responseErr = ErrRepoNotFound
} }
return output, NewResticError(command, output.AllLines(), errors.Join(err, responseErr)) return output.Lines, NewResticError(command, output.Lines, responseErr)
} }
return output, nil return output.Lines, nil
} }
func (rcmd Restic) Backup(files []string, opts BackupOpts) error { func (rcmd Restic) Backup(files []string, opts BackupOpts) error {
@ -348,40 +322,29 @@ func (rcmd Restic) Check() error {
return err return err
} }
func (rcmd Restic) Unlock(unlockOpts UnlockOpts) error {
_, err := rcmd.RunRestic("unlock", unlockOpts)
return err
}
type Snapshot struct { type Snapshot struct {
UID int `json:"uid"` UID int `json:"uid"`
GID int `json:"gid"` GID int `json:"gid"`
Time time.Time `json:"time"` Time string `json:"time"`
Tree string `json:"tree"` Tree string `json:"tree"`
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
Username string `json:"username"` Username string `json:"username"`
ID string `json:"id"` ID string `json:"id"`
ShortID string `json:"short_id"` //nolint:tagliatelle ShortID string `json:"short_id"` // nolint:tagliatelle
Paths []string `json:"paths"` Paths []string `json:"paths"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
} }
func (rcmd Restic) ReadSnapshots() ([]Snapshot, error) { func (rcmd Restic) ReadSnapshots() ([]Snapshot, error) {
output, err := rcmd.RunRestic("snapshots", GenericOpts{"--json"}) lines, err := rcmd.RunRestic("snapshots", GenericOpts{"--json"})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(output.Stdout.Lines) == 0 {
return nil, fmt.Errorf("no snapshot output to parse: %w", ErrRestic)
}
singleLineOutput := strings.Join(output.Stdout.Lines, "")
snapshots := new([]Snapshot) snapshots := new([]Snapshot)
if err = json.Unmarshal([]byte(singleLineOutput), snapshots); err != nil {
return nil, fmt.Errorf("failed parsing snapshot results from %s: %w", singleLineOutput, err) if err = json.Unmarshal([]byte(lines[0]), snapshots); err != nil {
return nil, fmt.Errorf("failed parsing snapshot results: %w", err)
} }
return *snapshots, nil return *snapshots, nil

View File

@ -32,12 +32,8 @@ func TestGlobalOptions(t *testing.T) {
LimitUpload: 1, LimitUpload: 1,
VerboseLevel: 1, VerboseLevel: 1,
CleanupCache: true, CleanupCache: true,
InsecureTLS: true,
NoCache: true, NoCache: true,
NoLock: true, NoLock: true,
Options: map[string]string{
"key": "a long value",
},
}.ToArgs() }.ToArgs()
expected := []string{ expected := []string{
@ -49,10 +45,8 @@ func TestGlobalOptions(t *testing.T) {
"--limit-upload", "1", "--limit-upload", "1",
"--verbose", "1", "--verbose", "1",
"--cleanup-cache", "--cleanup-cache",
"--insecure-tls",
"--no-cache", "--no-cache",
"--no-lock", "--no-lock",
"--option", "key='a long value'",
} }
AssertEqual(t, "args didn't match", expected, args) AssertEqual(t, "args didn't match", expected, args)
@ -152,20 +146,6 @@ func TestForgetOpts(t *testing.T) {
AssertEqual(t, "args didn't match", expected, args) 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) { func TestBuildEnv(t *testing.T) {
t.Parallel() t.Parallel()
@ -176,19 +156,19 @@ func TestBuildEnv(t *testing.T) {
}{ }{
{ {
name: "No Env", name: "No Env",
cmd: main.Restic{}, //nolint:exhaustruct cmd: main.Restic{}, // nolint:exhaustivestruct
expected: os.Environ(), expected: os.Environ(),
}, },
{ {
name: "SetEnv", name: "SetEnv",
cmd: main.Restic{ //nolint:exhaustruct cmd: main.Restic{ // nolint:exhaustivestruct
Env: map[string]string{"TestKey": "Value"}, Env: map[string]string{"TestKey": "Value"},
}, },
expected: append(os.Environ(), "TestKey=Value"), expected: append(os.Environ(), "TestKey=Value"),
}, },
{ {
name: "SetEnv", name: "SetEnv",
cmd: main.Restic{ //nolint:exhaustruct cmd: main.Restic{ // nolint:exhaustivestruct
Passphrase: "Shhhhhhhh!!", Passphrase: "Shhhhhhhh!!",
}, },
expected: append(os.Environ(), "RESTIC_PASSWORD=Shhhhhhhh!!"), expected: append(os.Environ(), "RESTIC_PASSWORD=Shhhhhhhh!!"),
@ -226,28 +206,25 @@ func TestResticInterface(t *testing.T) {
Repo: repoDir, Repo: repoDir,
Env: map[string]string{}, Env: map[string]string{},
Passphrase: "Correct.Horse.Battery.Staple", Passphrase: "Correct.Horse.Battery.Staple",
//nolint:exhaustruct // nolint:exhaustivestruct
GlobalOpts: &main.ResticGlobalOpts{ GlobalOpts: &main.ResticGlobalOpts{
CacheDir: cacheDir, CacheDir: cacheDir,
Options: map[string]string{
"s3.storage-class": "REDUCED_REDUNDANCY",
},
}, },
Cwd: dataDir, Cwd: dataDir,
} }
// Write test file to the data dir // 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) AssertEqualFail(t, "unexpected error writing to test file", nil, err)
// Make sure no existing repo is found // Make sure no existing repo is found
_, err = restic.ReadSnapshots() _, err = restic.ReadSnapshots()
if err == nil || !errors.Is(err, main.ErrRepoNotFound) { if err == nil || !errors.Is(err, main.ErrRepoNotFound) {
AssertEqualFail(t, "didn't get expected error for backup", main.ErrRepoNotFound.Error(), err.Error()) AssertEqualFail(t, "didn't get expected error for backup", main.ErrRepoNotFound, err)
} }
// Try to backup when repo is not initialized // Try to backup when repo is not initialized
err = restic.Backup([]string{dataDir}, main.BackupOpts{}) //nolint:exhaustruct err = restic.Backup([]string{dataDir}, main.BackupOpts{}) // nolint:exhaustivestruct
if !errors.Is(err, main.ErrRepoNotFound) { if !errors.Is(err, main.ErrRepoNotFound) {
AssertEqualFail(t, "unexpected error creating making backup", nil, err) AssertEqualFail(t, "unexpected error creating making backup", nil, err)
} }
@ -261,7 +238,7 @@ func TestResticInterface(t *testing.T) {
AssertEqualFail(t, "unexpected error reinitializing repo", nil, err) AssertEqualFail(t, "unexpected error reinitializing repo", nil, err)
// Backup for real this time // Backup for real this time
err = restic.Backup([]string{dataDir}, main.BackupOpts{Tags: []string{"test"}}) //nolint:exhaustruct err = restic.Backup([]string{dataDir}, main.BackupOpts{Tags: []string{"test"}}) // nolint:exhaustivestruct
AssertEqualFail(t, "unexpected error creating making backup", nil, err) AssertEqualFail(t, "unexpected error creating making backup", nil, err)
// Check snapshots // Check snapshots
@ -275,7 +252,7 @@ func TestResticInterface(t *testing.T) {
AssertEqual(t, "unexpected snapshot value: tags", []string{"test"}, snapshots[0].Tags) AssertEqual(t, "unexpected snapshot value: tags", []string{"test"}, snapshots[0].Tags)
// Backup again // Backup again
err = restic.Backup([]string{dataDir}, main.BackupOpts{}) //nolint:exhaustruct err = restic.Backup([]string{dataDir}, main.BackupOpts{}) // nolint:exhaustivestruct
AssertEqualFail(t, "unexpected error creating making second backup", nil, err) AssertEqualFail(t, "unexpected error creating making second backup", nil, err)
// Check for second backup // Check for second backup
@ -284,7 +261,7 @@ func TestResticInterface(t *testing.T) {
AssertEqual(t, "unexpected number of snapshots", 2, len(snapshots)) AssertEqual(t, "unexpected number of snapshots", 2, len(snapshots))
// Forget one backup // Forget one backup
err = restic.Forget(main.ForgetOpts{KeepLast: 1, Prune: true}) //nolint:exhaustruct err = restic.Forget(main.ForgetOpts{KeepLast: 1, Prune: true}) // nolint:exhaustivestruct
AssertEqualFail(t, "unexpected error forgetting snapshot", nil, err) AssertEqualFail(t, "unexpected error forgetting snapshot", nil, err)
// Check forgotten snapshot // Check forgotten snapshot
@ -297,7 +274,7 @@ func TestResticInterface(t *testing.T) {
AssertEqualFail(t, "unexpected error checking repo", nil, err) AssertEqualFail(t, "unexpected error checking repo", nil, err)
// Change the data file // 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) AssertEqualFail(t, "unexpected error writing to test file", nil, err)
// Check that data wrote // Check that data wrote
@ -306,15 +283,11 @@ func TestResticInterface(t *testing.T) {
AssertEqualFail(t, "incorrect value in test file (we expect the unexpected!)", "unexpected", string(value)) AssertEqualFail(t, "incorrect value in test file (we expect the unexpected!)", "unexpected", string(value))
// Restore files // Restore files
err = restic.Restore("latest", main.RestoreOpts{Target: restoreTarget}) //nolint:exhaustruct err = restic.Restore("latest", main.RestoreOpts{Target: restoreTarget}) // nolint:exhaustivestruct
AssertEqualFail(t, "unexpected error restoring latest snapshot", nil, err) AssertEqualFail(t, "unexpected error restoring latest snapshot", nil, err)
// Check restored values // Check restored values
value, err = os.ReadFile(restoredDataFile) value, err = os.ReadFile(restoredDataFile)
AssertEqualFail(t, "unexpected error reading from test file", nil, err) AssertEqualFail(t, "unexpected error reading from test file", nil, err)
AssertEqualFail(t, "incorrect value in test file", "testing", string(value)) 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" "github.com/robfig/cron/v3"
) )
var ( var jobResultsLock = sync.Mutex{}
jobResultsLock = sync.Mutex{} var jobResults = map[string]JobResult{}
jobResults = map[string]JobResult{}
)
type JobResult struct { type JobResult struct {
JobName string JobName string
@ -69,12 +67,9 @@ func healthHandleFunc(writer http.ResponseWriter, request *http.Request) {
func RunHTTPHandlers(addr string) error { func RunHTTPHandlers(addr string) error {
http.HandleFunc("/health", healthHandleFunc) http.HandleFunc("/health", healthHandleFunc)
http.Handle("/metrics", promhttp.HandlerFor( http.Handle("/metrics", promhttp.Handler())
Metrics.Registry,
promhttp.HandlerOpts{Registry: Metrics.Registry}, //nolint:exhaustruct
))
return fmt.Errorf("error on http server: %w", http.ListenAndServe(addr, nil)) //#nosec: g114 return fmt.Errorf("error on healthcheck: %w", http.ListenAndServe(addr, nil))
} }
func ScheduleAndRunJobs(jobs []Job) error { func ScheduleAndRunJobs(jobs []Job) error {
@ -115,8 +110,6 @@ func ScheduleAndRunJobs(jobs []Job) error {
defer func() { defer func() {
ctx := scheduler.Stop() ctx := scheduler.Stop()
<-ctx.Done() <-ctx.Done()
fmt.Println("All jobs successfully stopped")
}() }()
return nil return nil

View File

@ -5,7 +5,6 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"sort"
"strings" "strings"
) )
@ -40,7 +39,6 @@ func NewCapturedLogWriter(logger *log.Logger) *CapturedLogWriter {
return &CapturedLogWriter{Lines: []string{}, logger: logger} return &CapturedLogWriter{Lines: []string{}, logger: logger}
} }
// Write writes the provided byte slice to the logger and stores each captured line.
func (w *CapturedLogWriter) Write(content []byte) (n int, err error) { func (w *CapturedLogWriter) Write(content []byte) (n int, err error) {
message := string(content) message := string(content)
for _, line := range strings.Split(message, "\n") { for _, line := range strings.Split(message, "\n") {
@ -51,35 +49,8 @@ func (w *CapturedLogWriter) Write(content []byte) (n int, err error) {
return len(content), nil return len(content), nil
} }
// LinesMergedWith returns a slice of lines from this logger merged with another.
func (w CapturedLogWriter) LinesMergedWith(other CapturedLogWriter) []string {
allLines := []string{}
allLines = append(allLines, w.Lines...)
allLines = append(allLines, other.Lines...)
sort.Strings(allLines)
return allLines
}
type CapturedCommandLogWriter struct {
Stdout *CapturedLogWriter
Stderr *CapturedLogWriter
}
func NewCapturedCommandLogWriter(logger *log.Logger) *CapturedCommandLogWriter {
return &CapturedCommandLogWriter{
Stdout: NewCapturedLogWriter(logger),
Stderr: NewCapturedLogWriter(logger),
}
}
func (cclw CapturedCommandLogWriter) AllLines() []string {
return cclw.Stdout.LinesMergedWith(*cclw.Stderr)
}
func RunShell(script string, cwd string, env map[string]string, logger *log.Logger) error { func RunShell(script string, cwd string, env map[string]string, logger *log.Logger) error {
cmd := exec.Command("sh", "-c", strings.TrimSpace(script)) //nolint:gosec cmd := exec.Command("sh", "-c", strings.TrimSpace(script)) // nolint:gosec
// Make both stderr and stdout go to logger // Make both stderr and stdout go to logger
cmd.Stdout = NewCapturedLogWriter(logger) cmd.Stdout = NewCapturedLogWriter(logger)

238
tasks.go
View File

@ -67,17 +67,15 @@ func (t *JobTaskScript) SetName(name string) {
t.name = name t.name = name
} }
// JobTaskMySQL is a MySQL backup task that performs required pre and post tasks. // JobTaskMySQL is a sqlite backup task that performs required pre and post tasks.
type JobTaskMySQL struct { type JobTaskMySQL struct {
Port int `hcl:"port,optional"` Name string `hcl:"name,label"`
Name string `hcl:"name,label"` Hostname string `hcl:"hostname,optional"`
Hostname string `hcl:"hostname,optional"` Database string `hcl:"database,optional"`
Database string `hcl:"database,optional"` Username string `hcl:"username,optional"`
Username string `hcl:"username,optional"` Password string `hcl:"password,optional"`
Password string `hcl:"password,optional"` Tables []string `hcl:"tables,optional"`
Tables []string `hcl:"tables,optional"` DumpToPath string `hcl:"dump_to"`
NoTablespaces bool `hcl:"no_tablespaces,optional"`
DumpToPath string `hcl:"dump_to"`
} }
func (t JobTaskMySQL) Paths() []string { func (t JobTaskMySQL) Paths() []string {
@ -89,16 +87,11 @@ func (t JobTaskMySQL) Validate() error {
return fmt.Errorf("task %s is missing dump_to path: %w", t.Name, ErrMissingField) return fmt.Errorf("task %s is missing dump_to path: %w", t.Name, ErrMissingField)
} }
if stat, err := os.Stat(t.DumpToPath); err != nil { if s, err := os.Stat(t.DumpToPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf( return fmt.Errorf("task %s: invalid dump_to: could not stat path: %v: %w", t.Name, err, ErrInvalidConfigValue)
"task %s: invalid dump_to: could not stat path: %s: %w",
t.Name,
t.DumpToPath,
ErrInvalidConfigValue,
)
} }
} else if stat.Mode().IsDir() { } else if s.Mode().IsDir() {
return fmt.Errorf("task %s: dump_to cannot be a directory: %w", t.Name, ErrInvalidConfigValue) return fmt.Errorf("task %s: dump_to cannot be a directory: %w", t.Name, ErrInvalidConfigValue)
} }
@ -120,26 +113,16 @@ func (t JobTaskMySQL) GetPreTask() ExecutableTask {
command = append(command, "--host", t.Hostname) command = append(command, "--host", t.Hostname)
} }
if t.Port != 0 {
command = append(command, "--port", fmt.Sprintf("%d", t.Port))
}
if t.Username != "" { if t.Username != "" {
command = append(command, "--user", t.Username) command = append(command, "--user", t.Username)
} }
if t.Password != "" { if t.Password != "" {
command = append(command, fmt.Sprintf("--password=%s", t.Password)) command = append(command, "--password", t.Password)
}
if t.NoTablespaces {
command = append(command, "--no-tablespaces")
} }
if t.Database != "" { if t.Database != "" {
command = append(command, t.Database) command = append(command, t.Database)
} else {
command = append(command, "--all-databases")
} }
command = append(command, t.Tables...) command = append(command, t.Tables...)
@ -160,20 +143,12 @@ func (t JobTaskMySQL) GetPostTask() ExecutableTask {
command = append(command, "--host", t.Hostname) command = append(command, "--host", t.Hostname)
} }
if t.Port != 0 {
command = append(command, "--port", fmt.Sprintf("%d", t.Port))
}
if t.Username != "" { if t.Username != "" {
command = append(command, "--user", t.Username) command = append(command, "--user", t.Username)
} }
if t.Password != "" { if t.Password != "" {
command = append(command, fmt.Sprintf("--password=%s", t.Password)) command = append(command, "--password", t.Password)
}
if t.Database != "" {
command = append(command, t.Database)
} }
command = append(command, "<", t.DumpToPath) command = append(command, "<", t.DumpToPath)
@ -187,144 +162,6 @@ func (t JobTaskMySQL) GetPostTask() ExecutableTask {
} }
} }
// JobTaskPostgres is a postgres backup task that performs required pre and post tasks.
type JobTaskPostgres struct {
Port int `hcl:"port,optional"`
Name string `hcl:"name,label"`
Hostname string `hcl:"hostname,optional"`
Database string `hcl:"database,optional"`
Username string `hcl:"username,optional"`
Password string `hcl:"password,optional"`
Tables []string `hcl:"tables,optional"`
DumpToPath string `hcl:"dump_to"`
NoTablespaces bool `hcl:"no_tablespaces,optional"`
Clean bool `hcl:"clean,optional"`
Create bool `hcl:"create,optional"`
}
func (t JobTaskPostgres) Paths() []string {
return []string{t.DumpToPath}
}
func (t JobTaskPostgres) Validate() error {
if t.DumpToPath == "" {
return fmt.Errorf("task %s is missing dump_to path: %w", t.Name, ErrMissingField)
}
if stat, err := os.Stat(t.DumpToPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf(
"task %s: invalid dump_to: could not stat path: %s: %w",
t.Name,
t.DumpToPath,
ErrInvalidConfigValue,
)
}
} else if stat.Mode().IsDir() {
return fmt.Errorf("task %s: dump_to cannot be a directory: %w", t.Name, ErrInvalidConfigValue)
}
if len(t.Tables) > 0 && t.Database == "" {
return fmt.Errorf(
"task %s is invalid. Must specify a database to use tables: %w",
t.Name,
ErrMissingField,
)
}
return nil
}
//nolint:cyclop
func (t JobTaskPostgres) GetPreTask() ExecutableTask {
command := []string{"pg_dump"}
if t.Database == "" {
command = []string{"pg_dumpall"}
}
command = append(command, "--file", t.DumpToPath)
if t.Hostname != "" {
command = append(command, "--host", t.Hostname)
}
if t.Port != 0 {
command = append(command, "--port", fmt.Sprintf("%d", t.Port))
}
if t.Username != "" {
command = append(command, "--username", t.Username)
}
if t.NoTablespaces {
command = append(command, "--no-tablespaces")
}
if t.Clean {
command = append(command, "--clean")
}
if t.Create {
command = append(command, "--create")
}
for _, table := range t.Tables {
command = append(command, "--table", table)
}
if t.Database != "" {
command = append(command, t.Database)
}
env := map[string]string{}
if t.Password != "" {
env["PGPASSWORD"] = t.Password
}
return JobTaskScript{
name: t.Name,
env: env,
Cwd: ".",
OnBackup: strings.Join(command, " "),
OnRestore: "",
}
}
func (t JobTaskPostgres) GetPostTask() ExecutableTask {
command := []string{"psql"}
if t.Hostname != "" {
command = append(command, "--host", t.Hostname)
}
if t.Port != 0 {
command = append(command, "--port", fmt.Sprintf("%d", t.Port))
}
if t.Username != "" {
command = append(command, "--username", t.Username)
}
if t.Database != "" {
command = append(command, t.Database)
}
command = append(command, "<", t.DumpToPath)
env := map[string]string{}
if t.Password != "" {
env["PGPASSWORD"] = t.Password
}
return JobTaskScript{
name: t.Name,
env: env,
Cwd: ".",
OnBackup: "",
OnRestore: strings.Join(command, " "),
}
}
// JobTaskSqlite is a sqlite backup task that performs required pre and post tasks. // JobTaskSqlite is a sqlite backup task that performs required pre and post tasks.
type JobTaskSqlite struct { type JobTaskSqlite struct {
Name string `hcl:"name,label"` Name string `hcl:"name,label"`
@ -341,16 +178,11 @@ func (t JobTaskSqlite) Validate() error {
return fmt.Errorf("task %s is missing dump_to path: %w", t.Name, ErrMissingField) return fmt.Errorf("task %s is missing dump_to path: %w", t.Name, ErrMissingField)
} }
if stat, err := os.Stat(t.DumpToPath); err != nil { if s, err := os.Stat(t.DumpToPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf( return fmt.Errorf("task %s: invalid dump_to: could not stat path: %v: %w", t.Name, err, ErrInvalidConfigValue)
"task %s: invalid dump_to: could not stat path: %s: %w",
t.Name,
t.DumpToPath,
ErrInvalidConfigValue,
)
} }
} else if stat.Mode().IsDir() { } else if s.Mode().IsDir() {
return fmt.Errorf("task %s: dump_to cannot be a directory: %w", t.Name, ErrInvalidConfigValue) return fmt.Errorf("task %s: dump_to cannot be a directory: %w", t.Name, ErrInvalidConfigValue)
} }
@ -382,12 +214,11 @@ type BackupFilesTask struct {
BackupOpts *BackupOpts `hcl:"backup_opts,block"` BackupOpts *BackupOpts `hcl:"backup_opts,block"`
RestoreOpts *RestoreOpts `hcl:"restore_opts,block"` RestoreOpts *RestoreOpts `hcl:"restore_opts,block"`
name string name string
snapshot string
} }
func (t BackupFilesTask) RunBackup(cfg TaskConfig) error { func (t BackupFilesTask) RunBackup(cfg TaskConfig) error {
if t.BackupOpts == nil { if t.BackupOpts == nil {
t.BackupOpts = &BackupOpts{} //nolint:exhaustruct t.BackupOpts = &BackupOpts{} // nolint:exhaustivestruct
} }
if err := cfg.Restic.Backup(cfg.BackupPaths, *t.BackupOpts); err != nil { if err := cfg.Restic.Backup(cfg.BackupPaths, *t.BackupOpts); err != nil {
@ -402,14 +233,11 @@ func (t BackupFilesTask) RunBackup(cfg TaskConfig) error {
func (t BackupFilesTask) RunRestore(cfg TaskConfig) error { func (t BackupFilesTask) RunRestore(cfg TaskConfig) error {
if t.RestoreOpts == nil { if t.RestoreOpts == nil {
t.RestoreOpts = &RestoreOpts{} //nolint:exhaustruct t.RestoreOpts = &RestoreOpts{} // nolint:exhaustivestruct
} }
if t.snapshot == "" { // TODO: Make the snapshot configurable
t.snapshot = "latest" if err := cfg.Restic.Restore("latest", *t.RestoreOpts); err != nil {
}
if err := cfg.Restic.Restore(t.snapshot, *t.RestoreOpts); err != nil {
err = fmt.Errorf("failed restoring paths: %w", err) err = fmt.Errorf("failed restoring paths: %w", err)
cfg.Logger.Print(err) cfg.Logger.Print(err)
@ -437,16 +265,12 @@ func (t *BackupFilesTask) Validate() error {
// JobTask represents a single task within a backup job. // JobTask represents a single task within a backup job.
type JobTask struct { type JobTask struct {
Name string `hcl:"name,label"` Name string `hcl:"name,label"`
PreScripts []JobTaskScript `hcl:"pre_script,block"` PreScripts []JobTaskScript `hcl:"pre_script,block"`
PostScripts []JobTaskScript `hcl:"post_script,block"` PostScripts []JobTaskScript `hcl:"post_script,block"`
MySQL []JobTaskMySQL `hcl:"mysql,block"`
Postgres []JobTaskPostgres `hcl:"postgres,block"`
Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
} }
func (t JobTask) Validate() error { func (t JobTask) Validate() error {
// NOTE: Might make task types mutually exclusive because order is confusing even if deterministic
if t.Name == "" { if t.Name == "" {
return fmt.Errorf("task is missing a name: %w", ErrMissingField) return fmt.Errorf("task is missing a name: %w", ErrMissingField)
} }
@ -457,14 +281,6 @@ func (t JobTask) Validate() error {
func (t JobTask) GetPreTasks() []ExecutableTask { func (t JobTask) GetPreTasks() []ExecutableTask {
allTasks := []ExecutableTask{} allTasks := []ExecutableTask{}
for _, task := range t.MySQL {
allTasks = append(allTasks, task.GetPreTask())
}
for _, task := range t.Sqlite {
allTasks = append(allTasks, task.GetPreTask())
}
for _, exTask := range t.PreScripts { for _, exTask := range t.PreScripts {
exTask.SetName(t.Name) exTask.SetName(t.Name)
allTasks = append(allTasks, exTask) allTasks = append(allTasks, exTask)
@ -481,13 +297,5 @@ func (t JobTask) GetPostTasks() []ExecutableTask {
allTasks = append(allTasks, exTask) allTasks = append(allTasks, exTask)
} }
for _, task := range t.MySQL {
allTasks = append(allTasks, task.GetPostTask())
}
for _, task := range t.Sqlite {
allTasks = append(allTasks, task.GetPostTask())
}
return allTasks return allTasks
} }

View File

@ -15,7 +15,6 @@ func NewBufferedLogger(prefix string) (*bytes.Buffer, *log.Logger) {
return &outputBuffer, logger return &outputBuffer, logger
} }
func TestJobTaskScript(t *testing.T) { func TestJobTaskScript(t *testing.T) {
t.Parallel() t.Parallel()
@ -120,20 +119,20 @@ func TestJobTaskSql(t *testing.T) {
}{ }{
{ {
name: "mysql simple", name: "mysql simple",
//nolint:exhaustruct // nolint:exhaustivestruct
task: main.JobTaskMySQL{ task: main.JobTaskMySQL{
Name: "simple", Name: "simple",
DumpToPath: "./simple.sql", DumpToPath: "./simple.sql",
}, },
validationErr: nil, validationErr: nil,
preBackup: "mysqldump --result-file ./simple.sql --all-databases", preBackup: "mysqldump --result-file ./simple.sql",
postBackup: "", postBackup: "",
preRestore: "", preRestore: "",
postRestore: "mysql < ./simple.sql", postRestore: "mysql < ./simple.sql",
}, },
{ {
name: "mysql tables no database", name: "mysql tables no database",
//nolint:exhaustruct // nolint:exhaustivestruct
task: main.JobTaskMySQL{ task: main.JobTaskMySQL{
Name: "name", Name: "name",
Tables: []string{"table1", "table2"}, Tables: []string{"table1", "table2"},
@ -148,44 +147,19 @@ func TestJobTaskSql(t *testing.T) {
{ {
name: "mysql all options", name: "mysql all options",
task: main.JobTaskMySQL{ task: main.JobTaskMySQL{
Name: "simple", Name: "simple",
Hostname: "host", Hostname: "host",
Port: 3306, Username: "user",
Username: "user", Password: "pass",
Password: "pass", Database: "db",
Database: "db", Tables: []string{"table1", "table2"},
NoTablespaces: true, DumpToPath: "./simple.sql",
Tables: []string{"table1", "table2"},
DumpToPath: "./simple.sql",
}, },
validationErr: nil, validationErr: nil,
preBackup: "mysqldump --result-file ./simple.sql --host host --port 3306" + preBackup: "mysqldump --result-file ./simple.sql --host host --user user --password pass db table1 table2",
" --user user --password=pass --no-tablespaces db table1 table2", postBackup: "",
postBackup: "", preRestore: "",
preRestore: "", postRestore: "mysql --host host --user user --password pass < ./simple.sql",
postRestore: "mysql --host host --port 3306 --user user --password=pass db < ./simple.sql",
},
{
name: "psql all",
task: main.JobTaskPostgres{
Name: "simple",
Hostname: "host",
Port: 6543,
Username: "user",
Password: "pass",
Database: "db",
NoTablespaces: true,
Create: true,
Clean: true,
Tables: []string{"table1", "table2"},
DumpToPath: "./simple.sql",
},
validationErr: nil,
preBackup: "pg_dump --file ./simple.sql --host host --port 6543 --username user --no-tablespaces" +
" --clean --create --table table1 --table table2 db",
postBackup: "",
preRestore: "",
postRestore: "psql --host host --port 6543 --username user db < ./simple.sql",
}, },
// Sqlite // Sqlite
{ {

View File

@ -2,16 +2,6 @@ package main
import "fmt" 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 { func MergeEnvMap(parent, child map[string]string) map[string]string {
result := map[string]string{} result := map[string]string{}