Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
f9f00951a7 | |||
3d590997bc |
@ -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
5
.gitignore
vendored
@ -20,9 +20,4 @@ dist/
|
|||||||
|
|
||||||
# Built executable
|
# Built executable
|
||||||
restic-scheduler
|
restic-scheduler
|
||||||
resticscheduler
|
|
||||||
data/
|
data/
|
||||||
|
|
||||||
# Itest temp dirs
|
|
||||||
itest/data
|
|
||||||
itest/repo
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
|
22
Dockerfile
22
Dockerfile
@ -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" ]
|
|
||||||
|
19
Makefile
19
Makefile
@ -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
210
README.md
@ -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
18
go.mod
@ -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
34
go.sum
@ -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=
|
||||||
|
@ -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
|
|
@ -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
|
|
35
itest/run.sh
35
itest/run.sh
@ -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/*
|
|
@ -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 = "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
93
job.go
@ -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
|
||||||
}
|
}
|
||||||
|
137
job_test.go
137
job_test.go
@ -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
211
main.go
@ -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()
|
||||||
|
70
main_test.go
70
main_test.go
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
68
metrics.go
68
metrics.go
@ -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
123
restic.go
@ -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
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
15
scheduler.go
15
scheduler.go
@ -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
|
||||||
|
31
shell.go
31
shell.go
@ -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
238
tasks.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
10
utils.go
10
utils.go
@ -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{}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user