├── .gitignore ├── .dockerignore ├── examples ├── grafana.png ├── rules.yml └── grafana.json ├── restic ├── testdata │ ├── check_invalid_data.txt │ ├── forget_output.json │ └── snapshots.json ├── restic.go ├── check.go ├── check_test.go ├── forget_test.go ├── snapshot_test.go ├── helpers_test.go ├── snapshot.go └── forget.go ├── conf ├── testdata │ ├── config_invalid.yml │ ├── config_base.yml │ ├── config_passwordFile.yml │ └── config_from_file.yml ├── logging.go ├── config_test.go └── config.go ├── docker-compose.yml ├── Dockerfile ├── config.example.yml ├── LICENSE ├── main.go ├── .github └── workflows │ └── main.yml ├── go.mod ├── exporter ├── exporter.go └── collector.go ├── README.md ├── controller ├── integrity.go └── retention.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | config.yml -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | config.yml -------------------------------------------------------------------------------- /examples/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcardonne/restic-controller/HEAD/examples/grafana.png -------------------------------------------------------------------------------- /restic/testdata/check_invalid_data.txt: -------------------------------------------------------------------------------- 1 | error: error loading index 66c1a8dc: load : invalid data returned 2 | Fatal: LoadIndex returned errors -------------------------------------------------------------------------------- /conf/testdata/config_invalid.yml: -------------------------------------------------------------------------------- 1 | exporter: 2 | bind_address: ":8080" 3 | 4 | log: 5 | level: "info" 6 | 7 | repositories: 8 | - name: "backtothefuture" 9 | # Missing required fields 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | controller: 5 | image: tcardonne/restic-controller:latest 6 | ports: 7 | - "8080:8080" 8 | volumes: 9 | - "./config.yml:/app/config.yml" 10 | -------------------------------------------------------------------------------- /conf/testdata/config_base.yml: -------------------------------------------------------------------------------- 1 | exporter: 2 | bind_address: ":8080" 3 | 4 | log: 5 | level: "info" 6 | 7 | repositories: 8 | - name: "backtothefuture" 9 | url: "rest:https://user:password@repositories.restic.example/backtothefuture" 10 | password: "testtest" 11 | check: 12 | schedule: "* * * * *" 13 | retention: 14 | schedule: "* * * * *" 15 | policy: 16 | keep_last: 1 17 | -------------------------------------------------------------------------------- /restic/restic.go: -------------------------------------------------------------------------------- 1 | package restic 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | ) 7 | 8 | // Making possible to mock exec.CommandContext 9 | var execCommandContext = exec.CommandContext 10 | 11 | func buildCmdEnv(repositoryPassword string, env *map[string]string) []string { 12 | var cmdEnv []string 13 | cmdEnv = append(cmdEnv, "RESTIC_PASSWORD="+repositoryPassword) 14 | for k, v := range *env { 15 | cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v)) 16 | } 17 | 18 | return cmdEnv 19 | } 20 | -------------------------------------------------------------------------------- /conf/testdata/config_passwordFile.yml: -------------------------------------------------------------------------------- 1 | exporter: 2 | bind_address: ":8080" 3 | 4 | log: 5 | level: "info" 6 | 7 | repositories: 8 | - name: "backtothefuture" 9 | url: "rest:https://repositories.restic.example/backtothefuture" 10 | env_from_file: 11 | "RESTIC_REST_USERNAME": ./tmp-test-loadconfig-envfromfile 12 | password: "testtest" 13 | check: 14 | schedule: "* * * * *" 15 | retention: 16 | schedule: "* * * * *" 17 | policy: 18 | keep_last: 1 19 | -------------------------------------------------------------------------------- /conf/testdata/config_from_file.yml: -------------------------------------------------------------------------------- 1 | exporter: 2 | bind_address: ":8080" 3 | 4 | log: 5 | level: "info" 6 | 7 | repositories: 8 | - name: "backtothefuture" 9 | url: "rest:https://repositories.restic.example/backtothefuture" 10 | env_from_file: 11 | RESTIC_REST_PASSWORD: ./tmp-test-loadconfig-envfromfile 12 | password_file: ./tmp-test-loadconfig-envfromfile 13 | check: 14 | schedule: "* * * * *" 15 | retention: 16 | schedule: "* * * * *" 17 | policy: 18 | keep_last: 1 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Development image with go toolchain 2 | FROM golang:1.21-alpine AS builder 3 | 4 | RUN apk update && \ 5 | apk add alpine-sdk && \ 6 | rm -rf /var/cache/apk/* 7 | 8 | RUN mkdir -p /app 9 | WORKDIR /app 10 | 11 | COPY go.mod . 12 | COPY go.sum . 13 | RUN go mod download 14 | 15 | COPY . . 16 | RUN go build -o bin/restic-controller . 17 | 18 | # Actual released image 19 | FROM restic/restic:0.13.1 20 | 21 | RUN mkdir -p /app 22 | WORKDIR /app 23 | COPY --from=builder /app/bin/restic-controller . 24 | 25 | EXPOSE 8080 26 | 27 | ENTRYPOINT ["./restic-controller"] 28 | -------------------------------------------------------------------------------- /conf/logging.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // LogConfig specifies all the parameters needed for logging 10 | type LogConfig struct { 11 | Level string 12 | } 13 | 14 | // ConfigureLogging will take the logging configuration and also adds 15 | // a few default parameters 16 | func ConfigureLogging(config *LogConfig) error { 17 | level, err := logrus.ParseLevel(strings.ToUpper(config.Level)) 18 | if err != nil { 19 | return err 20 | } 21 | logrus.SetLevel(level) 22 | 23 | // always use the fulltimestamp 24 | logrus.SetFormatter(&logrus.TextFormatter{ 25 | FullTimestamp: true, 26 | DisableTimestamp: false, 27 | }) 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /examples/rules.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: ResticControllerGroup 3 | rules: 4 | - alert: IntegrityCheckFailure 5 | expr: restic_repo_integrity_check_status != 1 6 | for: 5m 7 | labels: 8 | severity: critical 9 | annotations: 10 | summary: "Integrity check failure (repository : {{ $labels.repository }})" 11 | description: "Integrity check failed for the repository {{ $labels.repository }}\n\n LABELS: {{ $labels }}" 12 | 13 | - alert: LatestSnapshotTooOld 14 | expr: time() - restic_group_snapshot_latest_seconds >= 90000 15 | labels: 16 | severity: warning 17 | annotations: 18 | summary: "Latest snapshot in repository {{ $labels.repository }} is older than 25h" 19 | description: "Latest snapshot in repository {{ $labels.repository }} is older than 25h.\nVerify backup job for this host.\n\nLABELS: {{ $labels }}" 20 | -------------------------------------------------------------------------------- /restic/check.go: -------------------------------------------------------------------------------- 1 | package restic 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // RunIntegrityCheck checks the integrity of a given repository. 14 | // Returns a boolean indicating if the repository is healthy. 15 | func RunIntegrityCheck(repository string, password string, env *map[string]string) (bool, error) { 16 | ctx := context.TODO() 17 | 18 | cmd := execCommandContext(ctx, "restic", "-r", repository, "check", "-q", "--no-lock") 19 | cmd.Env = append(cmd.Env, os.Environ()...) 20 | cmd.Env = append(cmd.Env, buildCmdEnv(password, env)...) 21 | 22 | log.WithFields(log.Fields{"component": "restic", "cmd": strings.Join(cmd.Args, " ")}).Debug("Running restic check command") 23 | _, err := cmd.Output() 24 | if err != nil { 25 | if exiterr, ok := err.(*exec.ExitError); ok { 26 | return false, fmt.Errorf("restic command returned with code %d : %s", exiterr.ExitCode(), exiterr.Stderr) 27 | } 28 | 29 | return false, err 30 | } 31 | 32 | return true, nil 33 | } 34 | -------------------------------------------------------------------------------- /restic/check_test.go: -------------------------------------------------------------------------------- 1 | package restic 2 | 3 | import ( 4 | "os/exec" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRunIntegrityCheck(t *testing.T) { 11 | execCommandContext = mockExecOutputString("", 0) 12 | defer func() { execCommandContext = exec.CommandContext }() 13 | 14 | health, err := RunIntegrityCheck(testResticRepository, testResticPassword, &testEnvMap) 15 | assert.NoError(t, err) 16 | 17 | assert.True(t, health) 18 | } 19 | 20 | func TestRunIntegrityCheck_InvalidIndex(t *testing.T) { 21 | execCommandContext = mockExecOutputFileStderr("testdata/check_invalid_data.txt", 1) 22 | defer func() { execCommandContext = exec.CommandContext }() 23 | 24 | health, err := RunIntegrityCheck(testResticRepository, testResticPassword, &testEnvMap) 25 | assert.Error(t, err) 26 | assert.Contains(t, err.Error(), "error: error loading index 66c1a8dc: load : invalid data returned") 27 | assert.False(t, health) 28 | } 29 | 30 | func TestRunIntegrityCheck_ExitError(t *testing.T) { 31 | execCommandContext = mockExecOutputString("", 1) 32 | defer func() { execCommandContext = exec.CommandContext }() 33 | 34 | health, err := RunIntegrityCheck(testResticRepository, testResticPassword, &testEnvMap) 35 | assert.Error(t, err) 36 | assert.False(t, health) 37 | } 38 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | exporter: 2 | # Prometheus exporter will listen on this address 3 | bind_address: ":8080" 4 | 5 | log: 6 | # Use debug to output executed restic commands 7 | level: "info" 8 | 9 | # Repositories configuration 10 | repositories: 11 | # Name of your repository 12 | - name: "local" 13 | # Restic repository URL 14 | url: "./repository" 15 | # Restic repository password 16 | password: "testtest" 17 | 18 | # Check configuration 19 | check: 20 | # Cron formatted schedule 21 | schedule: "* * * * *" 22 | 23 | # Run the check on startup (defaults to false) 24 | # run_on_startup: false 25 | 26 | retention: 27 | # Cron formatted schedule 28 | schedule: "* * * * *" 29 | 30 | # Run the check on startup (defaults to false) 31 | # run_on_startup: false 32 | 33 | # Retention policy, see restic docs for details 34 | policy: 35 | keep_last: 1 36 | # keep_daily: 3 37 | # keep_hourly: 1 38 | # keep_weekly: 1 39 | # keep_monthly: 1 40 | # keep_yearly: 1 41 | 42 | # Keep snapshots with tag1 OR tag2 43 | # keep_tags: ["tag1", "tag2"] 44 | # Keep snapshots with tag1 AND tag2 45 | # keep_tags: ["tag1,tag2"] 46 | 47 | # keep_within: "3y1m2d" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Thomas Cardonne 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/tcardonne/restic-controller/conf" 8 | "github.com/tcardonne/restic-controller/controller" 9 | "github.com/tcardonne/restic-controller/exporter" 10 | ) 11 | 12 | func main() { 13 | configFile := flag.String("config", "config.yml", "Specify a configuration file to load") 14 | flag.Parse() 15 | 16 | config, err := conf.LoadConfiguration(*configFile) 17 | if err != nil { 18 | log.WithField("err", err).Fatal("Failed to load configuration") 19 | } 20 | if err := conf.ConfigureLogging(&config.Log); err != nil { 21 | log.WithField("err", err).Fatal("Failed to configure logging") 22 | } 23 | 24 | integrityController := controller.NewIntegrityController(config.Repositories) 25 | retentionController := controller.NewRetentionController(config.Repositories) 26 | exp := exporter.NewExporter(config.Exporter, config.Repositories, integrityController, retentionController) 27 | 28 | if err := integrityController.Start(); err != nil { 29 | log.WithField("err", err).Fatal("Failed to start integrity controller") 30 | } 31 | 32 | if err := retentionController.Start(); err != nil { 33 | log.WithField("err", err).Fatal("Failed to start retention controller") 34 | } 35 | 36 | if err := exp.ListenAndServe(); err != nil { 37 | log.WithField("err", err).Fatal("Failed starting http server") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests, build and publish 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: [ v* ] 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up Go 1.21 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: ^1.21 16 | id: go 17 | 18 | - name: Check out code 19 | uses: actions/checkout@v2 20 | 21 | - name: Get dependencies 22 | run: go mod download 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | 27 | docker-build: 28 | runs-on: ubuntu-latest 29 | needs: [tests] 30 | 31 | steps: 32 | - name: Check out code 33 | uses: actions/checkout@v2 34 | 35 | - name: Build image 36 | run: docker build . -t image 37 | 38 | - name: Log into registry 39 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 40 | 41 | - name: Push image 42 | run: | 43 | IMAGE_ID=${{ secrets.DOCKER_USERNAME }}/restic-controller 44 | 45 | # Strip git ref prefix from version 46 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 47 | # Strip "v" prefix from tag name 48 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') 49 | # Use Docker `latest` tag convention 50 | [ "$VERSION" == "master" ] && VERSION=latest 51 | echo IMAGE_ID=$IMAGE_ID 52 | echo VERSION=$VERSION 53 | 54 | docker tag image $IMAGE_ID:$VERSION 55 | docker push $IMAGE_ID:$VERSION 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tcardonne/restic-controller 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/go-playground/validator/v10 v10.16.0 7 | github.com/prometheus/client_golang v1.17.0 8 | github.com/robfig/cron/v3 v3.0.1 9 | github.com/sirupsen/logrus v1.9.3 10 | github.com/spf13/viper v1.12.0 11 | github.com/stretchr/testify v1.8.4 12 | ) 13 | 14 | require ( 15 | github.com/beorn7/perks v1.0.1 // indirect 16 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 18 | github.com/fsnotify/fsnotify v1.7.0 // indirect 19 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 20 | github.com/go-playground/locales v0.14.1 // indirect 21 | github.com/go-playground/universal-translator v0.18.1 // indirect 22 | github.com/hashicorp/hcl v1.0.0 // indirect 23 | github.com/leodido/go-urn v1.2.4 // indirect 24 | github.com/magiconair/properties v1.8.7 // indirect 25 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 26 | github.com/mitchellh/mapstructure v1.5.0 // indirect 27 | github.com/pelletier/go-toml v1.9.5 // indirect 28 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 29 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 30 | github.com/prometheus/client_model v0.5.0 // indirect 31 | github.com/prometheus/common v0.45.0 // indirect 32 | github.com/prometheus/procfs v0.12.0 // indirect 33 | github.com/spf13/afero v1.10.0 // indirect 34 | github.com/spf13/cast v1.5.1 // indirect 35 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 36 | github.com/spf13/pflag v1.0.5 // indirect 37 | github.com/subosito/gotenv v1.6.0 // indirect 38 | golang.org/x/crypto v0.15.0 // indirect 39 | golang.org/x/net v0.18.0 // indirect 40 | golang.org/x/sys v0.14.0 // indirect 41 | golang.org/x/text v0.14.0 // indirect 42 | google.golang.org/protobuf v1.31.0 // indirect 43 | gopkg.in/ini.v1 v1.67.0 // indirect 44 | gopkg.in/yaml.v2 v2.4.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /conf/config_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLoadConfiguration_Base(t *testing.T) { 11 | config, err := LoadConfiguration("testdata/config_base.yml") 12 | 13 | assert.NoError(t, err) 14 | 15 | assert.Equal(t, ":8080", config.Exporter.BindAddress) 16 | assert.Equal(t, "info", config.Log.Level) 17 | assert.Equal(t, "backtothefuture", config.Repositories[0].Name) 18 | assert.Equal(t, "rest:https://user:password@repositories.restic.example/backtothefuture", config.Repositories[0].URL) 19 | assert.Equal(t, "testtest", config.Repositories[0].Password) 20 | assert.Equal(t, "* * * * *", config.Repositories[0].Check.Schedule) 21 | assert.Equal(t, "* * * * *", config.Repositories[0].Retention.Schedule) 22 | assert.Equal(t, 1, config.Repositories[0].Retention.Policy.KeepLast) 23 | } 24 | 25 | func TestLoadConfiguration_FromFile(t *testing.T) { 26 | filename := "./tmp-test-loadconfig-envfromfile" 27 | err := os.WriteFile(filename, []byte("someSecretValue"), 0644) 28 | defer os.Remove(filename) 29 | 30 | if err != nil { 31 | t.Logf("Failed to write tmp file: %s", err) 32 | t.FailNow() 33 | } 34 | config, err := LoadConfiguration("testdata/config_from_file.yml") 35 | 36 | if !assert.NoError(t, err) { 37 | t.FailNow() 38 | } 39 | 40 | assert.Equal(t, ":8080", config.Exporter.BindAddress) 41 | assert.Equal(t, "info", config.Log.Level) 42 | assert.Equal(t, "backtothefuture", config.Repositories[0].Name) 43 | assert.Equal(t, "rest:https://repositories.restic.example/backtothefuture", config.Repositories[0].URL) 44 | assert.Equal(t, "someSecretValue", config.Repositories[0].Env["RESTIC_REST_PASSWORD"]) 45 | assert.Equal(t, "someSecretValue", config.Repositories[0].Password) 46 | assert.Equal(t, "* * * * *", config.Repositories[0].Check.Schedule) 47 | assert.Equal(t, "* * * * *", config.Repositories[0].Retention.Schedule) 48 | assert.Equal(t, 1, config.Repositories[0].Retention.Policy.KeepLast) 49 | } 50 | 51 | func TestLoadConfiguration_Invalid(t *testing.T) { 52 | _, err := LoadConfiguration("testdata/config_invalid.yml") 53 | 54 | assert.Error(t, err) 55 | } 56 | -------------------------------------------------------------------------------- /exporter/exporter.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/promhttp" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/tcardonne/restic-controller/conf" 10 | "github.com/tcardonne/restic-controller/controller" 11 | ) 12 | 13 | // Exporter represents a Prometheus Exporter 14 | type Exporter struct { 15 | config conf.ExporterConfig 16 | repositories []*conf.Repository 17 | integrityController *controller.IntegrityController 18 | retentionController *controller.RetentionController 19 | } 20 | 21 | // NewExporter creates a new exporter 22 | func NewExporter(config conf.ExporterConfig, 23 | repositories []*conf.Repository, 24 | integrityController *controller.IntegrityController, 25 | retentionController *controller.RetentionController, 26 | ) *Exporter { 27 | return &Exporter{config, repositories, integrityController, retentionController} 28 | } 29 | 30 | func (exp *Exporter) handler(w http.ResponseWriter, r *http.Request) { 31 | registry := prometheus.NewRegistry() 32 | registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) 33 | registry.MustRegister(prometheus.NewGoCollector()) 34 | registry.MustRegister(newRepositoryCollector(r.Context(), exp.repositories, exp.integrityController, exp.retentionController)) 35 | 36 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 37 | h.ServeHTTP(w, r) 38 | } 39 | 40 | // ListenAndServe starts the Prometheus exporter endpoint 41 | func (exp *Exporter) ListenAndServe() error { 42 | http.HandleFunc("/metrics", exp.handler) 43 | 44 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 45 | _, _ = w.Write([]byte(` 46 | Restic Controller Exporter 47 | 48 |

Restic Controller Exporter

49 |

Metrics

50 | 51 | `)) 52 | }) 53 | 54 | log.WithFields(log.Fields{ 55 | "component": "exporter", 56 | "addr": exp.config.BindAddress, 57 | }).Info("Starting http server") 58 | 59 | return http.ListenAndServe(exp.config.BindAddress, nil) 60 | } 61 | -------------------------------------------------------------------------------- /restic/forget_test.go: -------------------------------------------------------------------------------- 1 | package restic 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRunForget(t *testing.T) { 12 | execCommandContext = mockExecOutputFile("testdata/forget_output.json", 0) 13 | defer func() { execCommandContext = exec.CommandContext }() 14 | 15 | policy := ForgetPolicy{KeepLast: 1} 16 | result, err := RunForget(testResticRepository, testResticPassword, &testEnvMap, &policy) 17 | assert.NoError(t, err) 18 | 19 | assert.Equal(t, 1, result.TotalKeep()) 20 | assert.Equal(t, 3, result.TotalRemove()) 21 | } 22 | 23 | func TestRunForget_InvalidJSON(t *testing.T) { 24 | execCommandContext = mockExecOutputString("invalidjson", 0) 25 | defer func() { execCommandContext = exec.CommandContext }() 26 | 27 | policy := ForgetPolicy{KeepLast: 1} 28 | result, err := RunForget(testResticRepository, testResticPassword, &testEnvMap, &policy) 29 | 30 | assert.Error(t, err) 31 | assert.Nil(t, result) 32 | } 33 | 34 | func TestRunForget_ExitError(t *testing.T) { 35 | execCommandContext = mockExecOutputString("", 1) 36 | defer func() { execCommandContext = exec.CommandContext }() 37 | 38 | policy := ForgetPolicy{KeepLast: 1} 39 | result, err := RunForget(testResticRepository, testResticPassword, &testEnvMap, &policy) 40 | 41 | assert.Error(t, err) 42 | assert.Nil(t, result) 43 | } 44 | 45 | func TestGetForgetPolicyArgs(t *testing.T) { 46 | testCases := []struct { 47 | policy ForgetPolicy 48 | want []string 49 | }{ 50 | // Simple 51 | {ForgetPolicy{KeepLast: 1}, []string{"--keep-last=1"}}, 52 | {ForgetPolicy{KeepDaily: 1}, []string{"--keep-daily=1"}}, 53 | {ForgetPolicy{KeepHourly: 1}, []string{"--keep-hourly=1"}}, 54 | {ForgetPolicy{KeepWeekly: 1}, []string{"--keep-weekly=1"}}, 55 | {ForgetPolicy{KeepMonthly: 1}, []string{"--keep-monthly=1"}}, 56 | {ForgetPolicy{KeepYearly: 1}, []string{"--keep-yearly=1"}}, 57 | {ForgetPolicy{KeepTags: []string{"tag1,tag2"}}, []string{"--keep-tags=tag1,tag2"}}, 58 | {ForgetPolicy{KeepTags: []string{"tag1", "tag2"}}, []string{"--keep-tags=tag1", "--keep-tags=tag2"}}, 59 | {ForgetPolicy{KeepWithin: "2y5m7d3h"}, []string{"--keep-within=2y5m7d3h"}}, 60 | // Complex 61 | {ForgetPolicy{KeepLast: 0, KeepDaily: 1}, []string{"--keep-daily=1"}}, 62 | {ForgetPolicy{KeepDaily: 10, KeepLast: 1}, []string{"--keep-last=1", "--keep-daily=10"}}, 63 | } 64 | 65 | for _, tc := range testCases { 66 | t.Run(strings.Join(tc.want, " "), func(t *testing.T) { 67 | result := getForgetPolicyArgs(&tc.policy) 68 | 69 | assert.Equal(t, tc.want, result) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /restic/snapshot_test.go: -------------------------------------------------------------------------------- 1 | package restic 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetSnapshotGroups(t *testing.T) { 12 | execCommandContext = mockExecOutputFile("testdata/snapshots.json", 0) 13 | defer func() { execCommandContext = exec.CommandContext }() 14 | 15 | ctx := context.TODO() 16 | groups, err := GetSnapshotGroups(ctx, testResticRepository, testResticPassword, &testEnvMap) 17 | assert.NoError(t, err) 18 | 19 | assert.Len(t, groups, 7) 20 | 21 | // Snapshot groups present in test fixture 22 | wantGroups := []struct { 23 | Key SnapshotGroupKey 24 | ExpectedSnapshotCount int 25 | }{ 26 | {SnapshotGroupKey{Hostname: "host1", Paths: "/", Tags: "tag1"}, 5}, 27 | {SnapshotGroupKey{Hostname: "host2", Paths: "/", Tags: "tag2"}, 7}, 28 | {SnapshotGroupKey{Hostname: "host2", Paths: "/,/backup", Tags: "tag1,tag2"}, 1}, 29 | {SnapshotGroupKey{Hostname: "host1", Paths: "/,/backup", Tags: ""}, 1}, 30 | {SnapshotGroupKey{Hostname: "host1", Paths: "/", Tags: "tag1,tag2"}, 1}, 31 | {SnapshotGroupKey{Hostname: "host1", Paths: "/backup", Tags: "tag1"}, 1}, 32 | {SnapshotGroupKey{Hostname: "host2", Paths: "/", Tags: "tag1"}, 2}, 33 | } 34 | 35 | for _, wg := range wantGroups { 36 | assert.Containsf(t, groups, wg.Key, `Key "%+v" not found`, wg.Key) 37 | assert.Lenf(t, groups[wg.Key], wg.ExpectedSnapshotCount, `Group "%+v" should have %d snapshots`, wg.Key, wg.ExpectedSnapshotCount) 38 | 39 | // Test sort. Index 0 should be most recent snapshot 40 | snapshots := groups[wg.Key] 41 | for i, sn := range snapshots[1:] { 42 | prevTime := snapshots[i].Time 43 | if prevTime.Before(sn.Time) { 44 | t.Errorf("Snapshot %s with time %s is more recent than previous one with time %s", sn.ShortID, sn.Time, prevTime) 45 | } 46 | } 47 | } 48 | } 49 | 50 | func TestGetSnapshotGroups_EmptyRepository(t *testing.T) { 51 | execCommandContext = mockExecOutputString(`[]`, 0) 52 | defer func() { execCommandContext = exec.CommandContext }() 53 | 54 | ctx := context.TODO() 55 | groups, err := GetSnapshotGroups(ctx, testResticRepository, testResticPassword, &testEnvMap) 56 | assert.NoError(t, err) 57 | assert.Empty(t, groups) 58 | } 59 | 60 | func TestGetSnapshotGroups_InvalidJSON(t *testing.T) { 61 | execCommandContext = mockExecOutputString(`invalid json`, 0) 62 | defer func() { execCommandContext = exec.CommandContext }() 63 | 64 | ctx := context.TODO() 65 | out, err := GetSnapshotGroups(ctx, testResticRepository, testResticPassword, &testEnvMap) 66 | assert.Error(t, err) 67 | assert.Nil(t, out) 68 | } 69 | 70 | func TestGetSnapshotGroups_ExitError(t *testing.T) { 71 | execCommandContext = mockExecOutputString("[]", 1) 72 | defer func() { execCommandContext = exec.CommandContext }() 73 | 74 | ctx := context.TODO() 75 | out, err := GetSnapshotGroups(ctx, testResticRepository, testResticPassword, &testEnvMap) 76 | assert.Error(t, err) 77 | assert.Nil(t, out) 78 | } 79 | -------------------------------------------------------------------------------- /restic/helpers_test.go: -------------------------------------------------------------------------------- 1 | package restic 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "testing" 11 | ) 12 | 13 | // Repositories targets are not used in reality for unit tests 14 | var testResticRepository = "rest:https://user:password@restic.domain.tld/test" 15 | var testResticPassword = "repositoryPassword" 16 | var testEnvMap = map[string]string{ 17 | "RESTIC_REST_USERNAME": "test", 18 | "RESTIC_REST_PASSWORD": "test", 19 | } 20 | 21 | // Mock exec.Command calls 22 | func mockExecOutputString(output string, exitCode int) func(context.Context, string, ...string) *exec.Cmd { 23 | return func(ctx context.Context, command string, args ...string) *exec.Cmd { 24 | cs := []string{"-test.run=TestHelperProcess", "--", command} 25 | cs = append(cs, args...) 26 | cmd := exec.CommandContext(ctx, os.Args[0], cs...) 27 | cmd.Env = []string{ 28 | "GO_WANT_HELPER_PROCESS=1", 29 | "GO_EXIT_CODE=" + strconv.Itoa(exitCode), 30 | "GO_OUTPUT=" + output, 31 | } 32 | return cmd 33 | } 34 | } 35 | 36 | func mockExecOutputFile(outputFile string, exitCode int) func(context.Context, string, ...string) *exec.Cmd { 37 | return func(ctx context.Context, command string, args ...string) *exec.Cmd { 38 | cs := []string{"-test.run=TestHelperProcess", "--", command} 39 | cs = append(cs, args...) 40 | cmd := exec.CommandContext(ctx, os.Args[0], cs...) 41 | cmd.Env = []string{ 42 | "GO_WANT_HELPER_PROCESS=1", 43 | "GO_EXIT_CODE=" + strconv.Itoa(exitCode), 44 | "GO_OUTPUT_FILE=" + outputFile, 45 | } 46 | return cmd 47 | } 48 | } 49 | 50 | func mockExecOutputFileStderr(outputFile string, exitCode int) func(context.Context, string, ...string) *exec.Cmd { 51 | return func(ctx context.Context, command string, args ...string) *exec.Cmd { 52 | cs := []string{"-test.run=TestHelperProcess", "--", command} 53 | cs = append(cs, args...) 54 | cmd := exec.CommandContext(ctx, os.Args[0], cs...) 55 | cmd.Env = []string{ 56 | "GO_WANT_HELPER_PROCESS=1", 57 | "GO_EXIT_CODE=" + strconv.Itoa(exitCode), 58 | "GO_OUTPUT_FILE=" + outputFile, 59 | "GO_OUTPUT_STDERR=1", 60 | } 61 | return cmd 62 | } 63 | } 64 | 65 | // TestHelperProcess is called by mocked exec.Command calls to simulate Restic invocations. 66 | func TestHelperProcess(t *testing.T) { 67 | if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 68 | return 69 | } 70 | 71 | exitCode, _ := strconv.Atoi(os.Getenv("GO_EXIT_CODE")) 72 | 73 | outputString := os.Getenv("GO_OUTPUT") 74 | if len(outputString) > 0 { 75 | fmt.Fprint(os.Stdout, outputString) 76 | os.Exit(exitCode) 77 | } 78 | 79 | outputFile := os.Getenv("GO_OUTPUT_FILE") 80 | if len(outputFile) > 0 { 81 | data, err := ioutil.ReadFile(outputFile) 82 | if err != nil { 83 | fmt.Fprintf(os.Stderr, "Loading test file error : %s", err) 84 | os.Exit(1) 85 | } 86 | if os.Getenv("GO_OUTPUT_STDERR") != "1" { 87 | os.Stdout.Write(data) 88 | } else { 89 | os.Stderr.Write(data) 90 | } 91 | os.Exit(exitCode) 92 | } 93 | 94 | os.Exit(exitCode) 95 | } 96 | -------------------------------------------------------------------------------- /conf/config.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-playground/validator/v10" 7 | "github.com/spf13/viper" 8 | "github.com/tcardonne/restic-controller/restic" 9 | ) 10 | 11 | // ExporterConfig contains general configuration for the Prometheus exporter 12 | type ExporterConfig struct { 13 | BindAddress string `mapstructure:"bind_address"` 14 | } 15 | 16 | // Repository contains configuration for one repository 17 | type Repository struct { 18 | Name string `mapstructure:"name" validate:"required"` 19 | URL string `mapstructure:"url" validate:"required"` 20 | Password string `mapstructure:"password" validate:"required_without=PasswordFile"` 21 | PasswordFile string `mapstructure:"password_file" validate:"required_without=Password"` 22 | EnvFromFile map[string]string `mapstructure:"env_from_file"` 23 | Env map[string]string `mapstructure:"env"` 24 | Check struct { 25 | Schedule string `mapstructure:"schedule" validate:"required"` 26 | RunOnStartup bool `mapstructure:"run_on_startup"` 27 | } `mapstructure:"check" validate:"required"` 28 | Retention struct { 29 | Schedule string `mapstructure:"schedule" validate:"required"` 30 | RunOnStartup bool `mapstructure:"run_on_startup"` 31 | Policy *restic.ForgetPolicy `mapstructure:"policy" validate:"required"` 32 | } `mapstructure:"retention" validate:"required"` 33 | } 34 | 35 | // Configuration is the root of the configuration 36 | type Configuration struct { 37 | Log LogConfig `mapstructure:"log"` 38 | Exporter ExporterConfig `mapstructure:"exporter"` 39 | Repositories []*Repository `mapstructure:"repositories" validate:"required,dive"` 40 | } 41 | 42 | // LoadConfiguration loads and validates the configuration from a file 43 | func LoadConfiguration(configFile string) (*Configuration, error) { 44 | configuration := Configuration{ 45 | Exporter: ExporterConfig{ 46 | BindAddress: "127.0.0.1:8080", 47 | }, 48 | } 49 | 50 | viper.SetConfigFile(configFile) 51 | if err := viper.ReadInConfig(); err != nil { 52 | return nil, err 53 | } 54 | 55 | err := viper.Unmarshal(&configuration) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | for _, r := range configuration.Repositories { 61 | if r.PasswordFile != "" { 62 | content, err := os.ReadFile(r.PasswordFile) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | r.Password = string(content) 68 | } 69 | 70 | if r.Env == nil { 71 | r.Env = make(map[string]string) 72 | } 73 | 74 | for k, v := range r.EnvFromFile { 75 | content, err := os.ReadFile(v) 76 | if err != nil { 77 | return nil, err 78 | } 79 | r.Env[k] = string(content) 80 | } 81 | } 82 | 83 | if err := validateConfiguration(&configuration); err != nil { 84 | return nil, err 85 | } 86 | 87 | return &configuration, nil 88 | } 89 | 90 | func validateConfiguration(config *Configuration) error { 91 | validate := validator.New() 92 | 93 | return validate.Struct(config) 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Restic Controller 2 | 3 | [![Tests, build and publish](https://github.com/tcardonne/restic-controller/workflows/Tests,%20build%20and%20publish/badge.svg)](https://github.com/tcardonne/restic-controller/actions) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/tcardonne/restic-controller)](https://goreportcard.com/report/github.com/tcardonne/restic-controller) 5 | [![Docker Pulls](https://img.shields.io/docker/pulls/tcardonne/restic-controller)](https://hub.docker.com/r/tcardonne/restic-controller) 6 | 7 | ## Introduction 8 | 9 | Restic Controller is a program that helps to monitor and manage [restic](https://github.com/restic/restic) backup repositories. 10 | 11 | This project has three components : 12 | - a Prometheus exporter, allowing to scrape metrics via Prometheus, 13 | - an integrity controller which will check repositories' integrity on a schedule, 14 | - a retention controller which will apply a given retention policy on a schedule. 15 | 16 | With this project, you can use a central location to monitor and manage your repositories. 17 | 18 | 19 | ## Get started 20 | 21 | Restic Controller is available as a Docker image : [tcardonne/restic-controller](https://hub.docker.com/repository/docker/tcardonne/restic-controller). 22 | 23 | Basic usage : 24 | ```bash 25 | docker run --rm -it \ 26 | -v "$PWD/config.yml:/app/config.yml" \ 27 | -p "8080:8080" \ 28 | tcardonne/restic-controller 29 | ``` 30 | 31 | Or, with `docker-compose.yml` : 32 | ```yaml 33 | version: '3.7' 34 | 35 | services: 36 | controller: 37 | image: tcardonne/restic-controller:latest 38 | ports: 39 | - "8080:8080" 40 | volumes: 41 | - "./config.yml:/app/config.yml" 42 | ``` 43 | 44 | 45 | Once started, the controller will run scheduled integrity checks and apply retention policies. Exported metrics are available on the `/metrics` path. 46 | 47 | ## Configuration 48 | 49 | Restic-Controller will, by default, look for a `config.yml` file in the current working directory. 50 | 51 | Full configuration reference available in the `config.example.yml` file. 52 | 53 | ### Example : 54 | 55 | ```yaml 56 | exporter: 57 | bind_address: ":8080" 58 | 59 | repositories: 60 | - name: "backtothefuture" 61 | url: "rest:https://user:password@repositories.restic.example/backtothefuture" 62 | # envFromFile: 63 | # RESTIC_REST_USERNAME: /etc/secrets/backtothefuture/username 64 | # RESTIC_REST_PASSWORD: /etc/secrets/backtothefuture/password 65 | password: "password" 66 | # passwordFile: "/etc/secrets/backtothefuture-repository" 67 | check: 68 | schedule: "0 3 * * *" 69 | retention: 70 | schedule: "0 4 * * *" 71 | policy: 72 | keep_last: 15 73 | ``` 74 | 75 | This configuration will run integrity checks everyday at 3AM and apply the retention policy at 4AM. The policy will keep the last 15 snapshots. 76 | 77 | ## Grafana & Alertmanager 78 | 79 | A Grafana dashboard and a sample Alertmanager rules file are available in the `examples/` directory. 80 | 81 | ![Grafana example](examples/grafana.png) 82 | 83 | 84 | ## License 85 | 86 | This project is licensed under [BSD 2-Clause License](https://opensource.org/licenses/BSD-2-Clause). You can find the complete text in the file `LICENSE`. -------------------------------------------------------------------------------- /controller/integrity.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/robfig/cron/v3" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/tcardonne/restic-controller/conf" 11 | "github.com/tcardonne/restic-controller/restic" 12 | ) 13 | 14 | // IntegrityReport represents an integrity report 15 | type IntegrityReport struct { 16 | Time *time.Time 17 | Healthy bool 18 | } 19 | 20 | // IntegrityController represents an instance of the integrity controller 21 | type IntegrityController struct { 22 | mu sync.RWMutex 23 | logger *log.Entry 24 | repositories []*conf.Repository 25 | integrityReports map[string]IntegrityReport 26 | } 27 | 28 | // NewIntegrityController creates a new IntegrityController 29 | func NewIntegrityController(repositories []*conf.Repository) *IntegrityController { 30 | reports := make(map[string]IntegrityReport) 31 | 32 | for _, repo := range repositories { 33 | reports[repo.Name] = IntegrityReport{} 34 | } 35 | 36 | return &IntegrityController{ 37 | logger: log.WithFields(log.Fields{"component": "controller/integrity"}), 38 | repositories: repositories, 39 | integrityReports: reports, 40 | } 41 | } 42 | 43 | // Start runs integrity checks in the background 44 | func (c *IntegrityController) Start() error { 45 | schedules := cron.New() 46 | for _, repository := range c.repositories { 47 | if repository.Check.Schedule == "" { 48 | continue 49 | } 50 | 51 | _, err := schedules.AddFunc(repository.Check.Schedule, c.RunCheck(repository)) 52 | if err != nil { 53 | return fmt.Errorf(`failed to add cron for repository "%s" with schedule "%s" : "%s"`, repository.Name, repository.Check.Schedule, err) 54 | } 55 | 56 | if repository.Check.RunOnStartup { 57 | go c.RunCheck(repository)() 58 | } 59 | } 60 | schedules.Start() 61 | 62 | return nil 63 | } 64 | 65 | // RunCheck runs an integrity check 66 | func (c *IntegrityController) RunCheck(repository *conf.Repository) func() { 67 | return func() { 68 | c.logger.WithField("repository", repository.Name).Info("Running integrity check") 69 | 70 | healthy, err := restic.RunIntegrityCheck(repository.URL, repository.Password, &repository.Env) 71 | c.setIntegrityReport(repository.Name, healthy) 72 | if err != nil { 73 | c.logger.WithFields(log.Fields{ 74 | "repository": repository.Name, 75 | "healthy": healthy, 76 | "err": err, 77 | }).Error("Integrity check reported unhealthy") 78 | } else { 79 | c.logger.WithFields(log.Fields{ 80 | "repository": repository.Name, 81 | "healthy": healthy, 82 | }).Info("Finished integrity check") 83 | } 84 | } 85 | } 86 | 87 | // GetIntegrityReport returns an integrity report 88 | func (c *IntegrityController) GetIntegrityReport(repositoryName string) (IntegrityReport, error) { 89 | c.mu.Lock() 90 | report, ok := c.integrityReports[repositoryName] 91 | c.mu.Unlock() 92 | if !ok { 93 | return IntegrityReport{}, fmt.Errorf("no repository for name %s", repositoryName) 94 | } 95 | 96 | return report, nil 97 | } 98 | 99 | func (c *IntegrityController) setIntegrityReport(repositoryName string, healthy bool) error { 100 | c.mu.Lock() 101 | report, ok := c.integrityReports[repositoryName] 102 | if !ok { 103 | c.mu.Unlock() 104 | return fmt.Errorf("no repository for name %s", repositoryName) 105 | } 106 | time := time.Now() 107 | report.Time = &time 108 | report.Healthy = healthy 109 | c.integrityReports[repositoryName] = report 110 | c.mu.Unlock() 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /restic/snapshot.go: -------------------------------------------------------------------------------- 1 | package restic 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "sort" 10 | "strings" 11 | "time" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // Snapshot represents a snapshot returned by restic 17 | type Snapshot struct { 18 | ID string `json:"id"` 19 | ShortID string `json:"short_id"` 20 | Hostname string `json:"hostname"` 21 | Paths []string `json:"paths"` 22 | Tags []string `json:"tags"` 23 | Time time.Time `json:"time"` 24 | Username string `json:"username"` 25 | } 26 | 27 | // Snapshots is a list of Snapshots 28 | type Snapshots []*Snapshot 29 | 30 | // Len returns the number of snapshots in sn. 31 | func (sn Snapshots) Len() int { 32 | return len(sn) 33 | } 34 | 35 | // Less returns true iff the ith snapshot has been made after the jth. 36 | func (sn Snapshots) Less(i, j int) bool { 37 | return sn[i].Time.After(sn[j].Time) 38 | } 39 | 40 | // Swap exchanges the two snapshots. 41 | func (sn Snapshots) Swap(i, j int) { 42 | sn[i], sn[j] = sn[j], sn[i] 43 | } 44 | 45 | // SnapshotGroupKey is used as a key when grouping snapshots 46 | type SnapshotGroupKey struct { 47 | Hostname string `json:"hostname"` 48 | Paths string `json:"paths"` 49 | Tags string `json:"tags"` 50 | } 51 | 52 | // SnapshotGroups is used a as a map of Snapshots grouped by SnapshotGroupKey 53 | type SnapshotGroups map[SnapshotGroupKey]Snapshots 54 | 55 | // TotalSnapshotsCount returns the total count of snapshots across all groups 56 | func (sg SnapshotGroups) TotalSnapshotsCount() int { 57 | var count int 58 | for _, g := range sg { 59 | count += len(g) 60 | } 61 | return count 62 | } 63 | 64 | // Sort will order snapshots in each group by date, newest to oldest. 65 | // Latest snapshots will then be on index 0. 66 | func (sg SnapshotGroups) Sort() { 67 | for _, g := range sg { 68 | sort.Sort(g) 69 | } 70 | } 71 | 72 | // GetSnapshotGroups returns the list of snapshots 73 | func GetSnapshotGroups(ctx context.Context, repository string, password string, env *map[string]string) (SnapshotGroups, error) { 74 | cmd := execCommandContext(ctx, "restic", "-r", repository, "snapshots", "--json", "--no-lock") 75 | cmd.Env = append(cmd.Env, os.Environ()...) 76 | cmd.Env = append(cmd.Env, buildCmdEnv(password, env)...) 77 | 78 | log.WithFields(log.Fields{"component": "restic", "cmd": strings.Join(cmd.Args, " ")}).Debug("Running restic snapshots command") 79 | output, err := cmd.Output() 80 | if err != nil { 81 | if ctx.Err() != nil { 82 | return nil, ctx.Err() 83 | } 84 | if exiterr, ok := err.(*exec.ExitError); ok { 85 | return nil, fmt.Errorf("restic command returned with code %d : %s", exiterr.ExitCode(), exiterr.Stderr) 86 | } 87 | 88 | return nil, err 89 | } 90 | 91 | var snapshots Snapshots 92 | if err := json.Unmarshal(output, &snapshots); err != nil { 93 | return nil, err 94 | } 95 | 96 | snapshotGroups := groupSnapshots(snapshots) 97 | snapshotGroups.Sort() 98 | 99 | return snapshotGroups, nil 100 | } 101 | 102 | func groupSnapshots(snapshots Snapshots) SnapshotGroups { 103 | // group by hostname and dirs 104 | snapshotGroups := make(SnapshotGroups) 105 | 106 | for _, sn := range snapshots { 107 | hostname := sn.Hostname 108 | paths := sn.Paths 109 | sort.StringSlice(paths).Sort() 110 | tags := sn.Tags 111 | sort.StringSlice(tags).Sort() 112 | 113 | groupKey := SnapshotGroupKey{ 114 | Hostname: hostname, 115 | Paths: strings.Join(paths, ","), 116 | Tags: strings.Join(tags, ","), 117 | } 118 | snapshotGroups[groupKey] = append(snapshotGroups[groupKey], sn) 119 | } 120 | 121 | return snapshotGroups 122 | } 123 | -------------------------------------------------------------------------------- /controller/retention.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/robfig/cron/v3" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/tcardonne/restic-controller/conf" 11 | "github.com/tcardonne/restic-controller/restic" 12 | ) 13 | 14 | // RetentionReport represents a report about a forget action 15 | type RetentionReport struct { 16 | Time *time.Time 17 | Kept int 18 | Removed int 19 | } 20 | 21 | // RetentionController represents an instance of the retention controller 22 | type RetentionController struct { 23 | mu sync.RWMutex 24 | logger *log.Entry 25 | repositories []*conf.Repository 26 | retentionReports map[string]RetentionReport 27 | } 28 | 29 | // NewRetentionController creates a new retention controller 30 | func NewRetentionController(repositories []*conf.Repository) *RetentionController { 31 | reports := make(map[string]RetentionReport) 32 | 33 | for _, repo := range repositories { 34 | reports[repo.Name] = RetentionReport{} 35 | } 36 | 37 | return &RetentionController{ 38 | logger: log.WithFields(log.Fields{"component": "controller/retention"}), 39 | repositories: repositories, 40 | retentionReports: reports, 41 | } 42 | } 43 | 44 | // Start applies retention policy periodically checks in the background 45 | func (c *RetentionController) Start() error { 46 | schedules := cron.New() 47 | for _, repository := range c.repositories { 48 | if repository.Retention.Schedule == "" { 49 | continue 50 | } 51 | 52 | _, err := schedules.AddFunc(repository.Retention.Schedule, c.RunForget(repository)) 53 | if err != nil { 54 | return fmt.Errorf(`failed to add cron for repository "%s" with schedule "%s" : "%s"`, repository.Name, repository.Retention.Schedule, err) 55 | } 56 | 57 | if repository.Retention.RunOnStartup { 58 | go c.RunForget(repository)() 59 | } 60 | } 61 | schedules.Start() 62 | 63 | return nil 64 | } 65 | 66 | // RunForget runs a forget action 67 | func (c *RetentionController) RunForget(repository *conf.Repository) func() { 68 | return func() { 69 | c.logger.WithField("repository", repository.Name).Info("Running forget") 70 | 71 | forgetResult, err := restic.RunForget(repository.URL, repository.Password, &repository.Env, repository.Retention.Policy) 72 | if err != nil { 73 | c.logger.WithFields(log.Fields{ 74 | "repository": repository.Name, 75 | "err": err, 76 | }).Error("Forget failed") 77 | } else { 78 | c.setRetentionReport(repository.Name, forgetResult.TotalKeep(), forgetResult.TotalRemove()) 79 | c.logger.WithFields(log.Fields{ 80 | "repository": repository.Name, 81 | "removed": forgetResult.TotalRemove(), 82 | "kept": forgetResult.TotalKeep(), 83 | }).Info("Finished forget") 84 | } 85 | } 86 | } 87 | 88 | // GetRetentionReport returns a retention report 89 | func (c *RetentionController) GetRetentionReport(repositoryName string) (RetentionReport, error) { 90 | c.mu.Lock() 91 | report, ok := c.retentionReports[repositoryName] 92 | c.mu.Unlock() 93 | if !ok { 94 | return RetentionReport{}, fmt.Errorf("no repository for name %s", repositoryName) 95 | } 96 | 97 | return report, nil 98 | } 99 | 100 | func (c *RetentionController) setRetentionReport(repositoryName string, kept int, removed int) error { 101 | c.mu.Lock() 102 | report, ok := c.retentionReports[repositoryName] 103 | if !ok { 104 | c.mu.Unlock() 105 | return fmt.Errorf("no repository for name %s", repositoryName) 106 | } 107 | time := time.Now() 108 | report.Time = &time 109 | report.Kept = kept 110 | report.Removed = removed 111 | c.retentionReports[repositoryName] = report 112 | c.mu.Unlock() 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /restic/testdata/forget_output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tags": null, 4 | "host": "DESKTOP-MARTY", 5 | "paths": [ 6 | "C:\\Users\\backtothefuture\\Projects\\restic-controller\\conf" 7 | ], 8 | "keep": [ 9 | { 10 | "time": "2020-05-05T00:35:02.8871781+02:00", 11 | "parent": "ea6626bd98343f32a8927e9e200b3880bbfd192746cfdc086036bd3afe44c091", 12 | "tree": "c1a9d82f00ca2431e13b60f55fea95b4625f8a591efb4bf5e7d93d470f231540", 13 | "paths": [ 14 | "C:\\Users\\backtothefuture\\Projects\\restic-controller\\conf" 15 | ], 16 | "hostname": "DESKTOP-MARTY", 17 | "username": "DESKTOP-MARTY\\backtothefuture", 18 | "id": "060eb7c4b74d67ace66c57d009025fab4d92f4f0284b3b19f6b278eb76a7ea86", 19 | "short_id": "060eb7c4" 20 | } 21 | ], 22 | "remove": [ 23 | { 24 | "time": "2020-05-05T00:34:51.5411174+02:00", 25 | "parent": "d2ce359ca761a46eeb62b1a7bf30d6897b88d7114b83bc5718f30ecb18b92cd0", 26 | "tree": "c1a9d82f00ca2431e13b60f55fea95b4625f8a591efb4bf5e7d93d470f231540", 27 | "paths": [ 28 | "C:\\Users\\backtothefuture\\Projects\\restic-controller\\conf" 29 | ], 30 | "hostname": "DESKTOP-MARTY", 31 | "username": "DESKTOP-MARTY\\backtothefuture", 32 | "id": "ea6626bd98343f32a8927e9e200b3880bbfd192746cfdc086036bd3afe44c091", 33 | "short_id": "ea6626bd" 34 | }, 35 | { 36 | "time": "2020-05-05T00:34:45.5981657+02:00", 37 | "parent": "bb525fd224f0f7fcdfe437f6f33f43a755a1cf02f889b0cd0a370d64d2a93c25", 38 | "tree": "c1a9d82f00ca2431e13b60f55fea95b4625f8a591efb4bf5e7d93d470f231540", 39 | "paths": [ 40 | "C:\\Users\\backtothefuture\\Projects\\restic-controller\\conf" 41 | ], 42 | "hostname": "DESKTOP-MARTY", 43 | "username": "DESKTOP-MARTY\\backtothefuture", 44 | "id": "d2ce359ca761a46eeb62b1a7bf30d6897b88d7114b83bc5718f30ecb18b92cd0", 45 | "short_id": "d2ce359c" 46 | }, 47 | { 48 | "time": "2020-05-05T00:32:42.5922378+02:00", 49 | "parent": "2f1f03c1d435c190d2633685f596ce2c8795cd250781b51d3a8385f3e71ed814", 50 | "tree": "c1a9d82f00ca2431e13b60f55fea95b4625f8a591efb4bf5e7d93d470f231540", 51 | "paths": [ 52 | "C:\\Users\\backtothefuture\\Projects\\restic-controller\\conf" 53 | ], 54 | "hostname": "DESKTOP-MARTY", 55 | "username": "DESKTOP-MARTY\\backtothefuture", 56 | "id": "bb525fd224f0f7fcdfe437f6f33f43a755a1cf02f889b0cd0a370d64d2a93c25", 57 | "short_id": "bb525fd2" 58 | } 59 | ], 60 | "reasons": [ 61 | { 62 | "snapshot": { 63 | "time": "2020-05-05T00:35:02.8871781+02:00", 64 | "parent": "ea6626bd98343f32a8927e9e200b3880bbfd192746cfdc086036bd3afe44c091", 65 | "tree": "c1a9d82f00ca2431e13b60f55fea95b4625f8a591efb4bf5e7d93d470f231540", 66 | "paths": [ 67 | "C:\\Users\\backtothefuture\\Projects\\restic-controller\\conf" 68 | ], 69 | "hostname": "DESKTOP-MARTY", 70 | "username": "DESKTOP-MARTY\\backtothefuture" 71 | }, 72 | "matches": [ 73 | "last snapshot" 74 | ], 75 | "counters": {} 76 | } 77 | ] 78 | } 79 | ] -------------------------------------------------------------------------------- /restic/forget.go: -------------------------------------------------------------------------------- 1 | package restic 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // ForgetPolicy specifies to restic retention policy rules 16 | type ForgetPolicy struct { 17 | KeepLast int `mapstructure:"keep_last"` 18 | KeepDaily int `mapstructure:"keep_daily"` 19 | KeepHourly int `mapstructure:"keep_hourly"` 20 | KeepWeekly int `mapstructure:"keep_weekly"` 21 | KeepMonthly int `mapstructure:"keep_monthly"` 22 | KeepYearly int `mapstructure:"keep_yearly"` 23 | KeepTags []string `mapstructure:"keep_tags"` 24 | KeepWithin string `mapstructure:"keep_within"` 25 | } 26 | 27 | // ForgetResult is the aggregate of result by group of the forget action 28 | type ForgetResult struct { 29 | GroupResults []ForgetGroupResult 30 | } 31 | 32 | // TotalRemove returns the total of snapshots deleted during the forget action 33 | func (r *ForgetResult) TotalRemove() int { 34 | var total int 35 | for _, v := range r.GroupResults { 36 | total += len(v.Remove) 37 | } 38 | return total 39 | } 40 | 41 | // TotalKeep returns the total of snapshots kept during the forget action 42 | func (r *ForgetResult) TotalKeep() int { 43 | var total int 44 | for _, v := range r.GroupResults { 45 | total += len(v.Keep) 46 | } 47 | return total 48 | } 49 | 50 | // ForgetGroupResult contains group output for the forget action 51 | type ForgetGroupResult struct { 52 | Tags string `json:"tags"` 53 | Host string `json:"host"` 54 | Paths []string `json:"paths"` 55 | Keep []Snapshot `json:"keep"` 56 | Remove []Snapshot `json:"remove"` 57 | } 58 | 59 | // RunForget calls restic to run the forget command according to the given policy 60 | func RunForget(repository string, password string, env *map[string]string, policy *ForgetPolicy) (*ForgetResult, error) { 61 | ctx := context.TODO() 62 | 63 | args := []string{ 64 | "-r", repository, 65 | "forget", 66 | "--prune", 67 | "--json", 68 | "-q", 69 | } 70 | policyArgs := getForgetPolicyArgs(policy) 71 | args = append(args, policyArgs...) 72 | 73 | cmd := execCommandContext(ctx, "restic", args...) 74 | cmd.Env = append(cmd.Env, os.Environ()...) 75 | cmd.Env = append(cmd.Env, buildCmdEnv(password, env)...) 76 | 77 | log.WithFields(log.Fields{"component": "restic", "cmd": strings.Join(cmd.Args, " ")}).Debug("Running restic forget command") 78 | output, err := cmd.Output() 79 | 80 | if err != nil { 81 | if exiterr, ok := err.(*exec.ExitError); ok { 82 | return nil, fmt.Errorf("restic command returned with code %d : %s", exiterr.ExitCode(), exiterr.Stderr) 83 | } 84 | 85 | return nil, err 86 | } 87 | 88 | groupResults := []ForgetGroupResult{} 89 | if err := json.Unmarshal(output, &groupResults); err != nil { 90 | return nil, err 91 | } 92 | 93 | result := ForgetResult{GroupResults: groupResults} 94 | return &result, nil 95 | } 96 | 97 | func getForgetPolicyArgs(policy *ForgetPolicy) []string { 98 | var args []string 99 | 100 | if policy.KeepLast != 0 { 101 | args = append(args, "--keep-last="+strconv.Itoa(policy.KeepLast)) 102 | } 103 | 104 | if policy.KeepDaily != 0 { 105 | args = append(args, "--keep-daily="+strconv.Itoa(policy.KeepDaily)) 106 | } 107 | 108 | if policy.KeepHourly != 0 { 109 | args = append(args, "--keep-hourly="+strconv.Itoa(policy.KeepHourly)) 110 | } 111 | 112 | if policy.KeepWeekly != 0 { 113 | args = append(args, "--keep-weekly="+strconv.Itoa(policy.KeepWeekly)) 114 | } 115 | 116 | if policy.KeepMonthly != 0 { 117 | args = append(args, "--keep-monthly="+strconv.Itoa(policy.KeepMonthly)) 118 | } 119 | 120 | if policy.KeepYearly != 0 { 121 | args = append(args, "--keep-yearly="+strconv.Itoa(policy.KeepYearly)) 122 | } 123 | 124 | if len(policy.KeepTags) > 0 { 125 | for _, v := range policy.KeepTags { 126 | args = append(args, "--keep-tags="+v) 127 | } 128 | } 129 | 130 | if len(policy.KeepWithin) > 0 { 131 | args = append(args, "--keep-within="+policy.KeepWithin) 132 | } 133 | 134 | return args 135 | } 136 | -------------------------------------------------------------------------------- /exporter/collector.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/tcardonne/restic-controller/conf" 11 | "github.com/tcardonne/restic-controller/controller" 12 | "github.com/tcardonne/restic-controller/restic" 13 | ) 14 | 15 | type repositoryCollector struct { 16 | ctx context.Context 17 | repositories []*conf.Repository 18 | integrityController *controller.IntegrityController 19 | retentionController *controller.RetentionController 20 | logger *log.Entry 21 | 22 | errorMetric *prometheus.Desc 23 | scrapeDurationMetric *prometheus.Desc 24 | 25 | repoSnapshotsTotalMetric *prometheus.Desc 26 | repoSnapshotTimestampMetric *prometheus.Desc 27 | groupSnapshotsTotalMetric *prometheus.Desc 28 | groupLatestSnapshotTimestampMetric *prometheus.Desc 29 | 30 | repoIntegrityStatusMetric *prometheus.Desc 31 | repoIntegrityStatusLatestMetric *prometheus.Desc 32 | 33 | repoRetentionForgetKeptMetric *prometheus.Desc 34 | repoRetentionForgetRemovedMetric *prometheus.Desc 35 | repoRetentionForgetLatestMetric *prometheus.Desc 36 | } 37 | 38 | // Initializes every descriptor and returns a pointer to the collector 39 | func newRepositoryCollector(ctx context.Context, 40 | repositories []*conf.Repository, 41 | integrityController *controller.IntegrityController, 42 | retentionController *controller.RetentionController, 43 | ) *repositoryCollector { 44 | return &repositoryCollector{ 45 | ctx: ctx, 46 | repositories: repositories, 47 | integrityController: integrityController, 48 | retentionController: retentionController, 49 | logger: log.WithFields(log.Fields{"component": "exporter/collector"}), 50 | 51 | errorMetric: prometheus.NewDesc("restic_error", "Error occurred when trying to collect metrics", nil, nil), 52 | scrapeDurationMetric: prometheus.NewDesc("restic_scrape_duration_seconds", 53 | "Total time in seconds spent to collect all metrics", 54 | nil, nil, 55 | ), 56 | 57 | repoSnapshotsTotalMetric: prometheus.NewDesc("restic_repo_snapshots_total", 58 | "Total count of snapshots in the repository", 59 | []string{"repository"}, nil, 60 | ), 61 | repoSnapshotTimestampMetric: prometheus.NewDesc("restic_repo_snapshot_datetime_seconds", 62 | "Number of seconds since 1970 of snapshot's datetime", 63 | []string{"repository", "host", "paths", "tags", "short_id"}, nil, 64 | ), 65 | groupSnapshotsTotalMetric: prometheus.NewDesc("restic_group_snapshots_total", 66 | "Total count of snapshots in a group", 67 | []string{"repository", "host", "paths", "tags"}, nil, 68 | ), 69 | groupLatestSnapshotTimestampMetric: prometheus.NewDesc("restic_group_snapshot_latest_seconds", 70 | "Number of seconds since 1970 of last snapshot", 71 | []string{"repository", "host", "paths", "tags"}, nil, 72 | ), 73 | 74 | repoIntegrityStatusMetric: prometheus.NewDesc("restic_repo_integrity_status", 75 | "Status of the repository (healthy or unhealthy)", 76 | []string{"repository"}, nil, 77 | ), 78 | repoIntegrityStatusLatestMetric: prometheus.NewDesc("restic_repo_integrity_status_latest_seconds", 79 | "Number of seconds since 1970 of last integrity check", 80 | []string{"repository"}, nil, 81 | ), 82 | 83 | repoRetentionForgetKeptMetric: prometheus.NewDesc("restic_repo_retention_forget_kept_total", 84 | "Number of snapshots kept after last forget action", 85 | []string{"repository"}, nil, 86 | ), 87 | repoRetentionForgetRemovedMetric: prometheus.NewDesc("restic_repo_retention_forget_removed_total", 88 | "Number of snapshots deleted after last forget action", 89 | []string{"repository"}, nil, 90 | ), 91 | repoRetentionForgetLatestMetric: prometheus.NewDesc("restic_repo_retention_forget_latest_seconds", 92 | "Number of seconds since 1970 of last forget action", 93 | []string{"repository"}, nil, 94 | ), 95 | } 96 | } 97 | 98 | // Each and every collector must implement the Describe function. 99 | // It essentially writes all descriptors to the prometheus desc channel. 100 | func (c *repositoryCollector) Describe(ch chan<- *prometheus.Desc) { 101 | //Update this section with the each metric you create for a given collector 102 | ch <- c.repoSnapshotsTotalMetric 103 | } 104 | 105 | // Collect implements required collect function for all promehteus collectors 106 | func (c *repositoryCollector) Collect(ch chan<- prometheus.Metric) { 107 | start := time.Now() 108 | 109 | var wg sync.WaitGroup 110 | for _, repository := range c.repositories { 111 | wg.Add(1) 112 | go c.CollectRepository(repository, ch, &wg) 113 | } 114 | wg.Wait() 115 | 116 | elapsted := time.Since(start) 117 | ch <- prometheus.MustNewConstMetric(c.scrapeDurationMetric, prometheus.GaugeValue, elapsted.Seconds()) 118 | } 119 | 120 | func (c *repositoryCollector) CollectRepository(repository *conf.Repository, ch chan<- prometheus.Metric, wg *sync.WaitGroup) { 121 | c.logger.WithField("repository", repository.Name).Info("Starting collection") 122 | 123 | // Snapshot metrics 124 | groups, err := restic.GetSnapshotGroups(c.ctx, repository.URL, repository.Password, &repository.Env) 125 | if err != nil { 126 | c.logger.WithFields(log.Fields{"repository": repository.Name, "err": err}).Error("Error occurred when fetching restic snapshot list") 127 | ch <- prometheus.NewInvalidMetric(c.errorMetric, err) 128 | wg.Done() 129 | return 130 | } 131 | 132 | ch <- prometheus.MustNewConstMetric(c.repoSnapshotsTotalMetric, prometheus.GaugeValue, 133 | float64(groups.TotalSnapshotsCount()), 134 | repository.Name, 135 | ) 136 | 137 | for key, snapshots := range groups { 138 | ch <- prometheus.MustNewConstMetric(c.groupSnapshotsTotalMetric, prometheus.GaugeValue, 139 | float64(len(snapshots)), 140 | repository.Name, key.Hostname, key.Paths, key.Tags, 141 | ) 142 | 143 | if len(snapshots) > 0 { 144 | ch <- prometheus.MustNewConstMetric(c.groupLatestSnapshotTimestampMetric, prometheus.CounterValue, 145 | float64(snapshots[0].Time.Unix()), 146 | repository.Name, key.Hostname, key.Paths, key.Tags, 147 | ) 148 | } 149 | 150 | for _, snapshot := range snapshots { 151 | ch <- prometheus.MustNewConstMetric(c.repoSnapshotTimestampMetric, prometheus.CounterValue, 152 | float64(snapshot.Time.Unix()), 153 | repository.Name, key.Hostname, key.Paths, key.Tags, snapshot.ShortID, 154 | ) 155 | } 156 | } 157 | 158 | // Integrity metrics 159 | report, err := c.integrityController.GetIntegrityReport(repository.Name) 160 | if err != nil { 161 | c.logger.WithFields(log.Fields{"repository": repository.Name, "err": err}).Error("Error occurred whe fetching integrity status from controller") 162 | ch <- prometheus.NewInvalidMetric(c.errorMetric, err) 163 | wg.Done() 164 | return 165 | } 166 | if report.Time != nil { 167 | ch <- prometheus.MustNewConstMetric(c.repoIntegrityStatusLatestMetric, prometheus.CounterValue, 168 | float64(report.Time.Unix()), 169 | repository.Name, 170 | ) 171 | var healthy float64 172 | if report.Healthy { 173 | healthy = 1.0 174 | } else { 175 | healthy = 0.0 176 | } 177 | ch <- prometheus.MustNewConstMetric(c.repoIntegrityStatusMetric, prometheus.GaugeValue, 178 | healthy, 179 | repository.Name, 180 | ) 181 | } 182 | 183 | // Retention forget metrics 184 | retentionReport, err := c.retentionController.GetRetentionReport(repository.Name) 185 | if err != nil { 186 | c.logger.WithFields(log.Fields{"repository": repository.Name, "err": err}).Error("Error occurred whe fetching retention report from controller") 187 | ch <- prometheus.NewInvalidMetric(c.errorMetric, err) 188 | wg.Done() 189 | return 190 | } 191 | if retentionReport.Time != nil { 192 | ch <- prometheus.MustNewConstMetric(c.repoRetentionForgetLatestMetric, prometheus.CounterValue, 193 | float64(retentionReport.Time.Unix()), 194 | repository.Name, 195 | ) 196 | ch <- prometheus.MustNewConstMetric(c.repoRetentionForgetKeptMetric, prometheus.GaugeValue, 197 | float64(retentionReport.Kept), 198 | repository.Name, 199 | ) 200 | ch <- prometheus.MustNewConstMetric(c.repoRetentionForgetRemovedMetric, prometheus.GaugeValue, 201 | float64(retentionReport.Removed), 202 | repository.Name, 203 | ) 204 | } 205 | 206 | c.logger.WithField("repository", repository.Name).Info("Finished collection") 207 | wg.Done() 208 | } 209 | -------------------------------------------------------------------------------- /restic/testdata/snapshots.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "time": "2020-04-24T01:00:01.494966615+02:00", 4 | "tree": "819bd7be4e69afa81f48ff81e0a8d925386670fb05d0657c82d4b2c57a7da21e", 5 | "paths": [ 6 | "/" 7 | ], 8 | "hostname": "host1", 9 | "username": "root", 10 | "tags": [ 11 | "tag1" 12 | ], 13 | "id": "a5b59b0e01f37ab6d004a547d818dee891e285be3e5e1f00aa190f94c5a61237", 14 | "short_id": "a5b59b0e" 15 | }, 16 | { 17 | "time": "2020-04-14T22:54:53.314919466+02:00", 18 | "parent": "a5b59b0e01f37ab6d004a547d818dee891e285be3e5e1f00aa190f94c5a61237", 19 | "tree": "fae2f7a06700b9f2fdec7f212c69fb83525c2cb762485e15e7887d8fa16ce066", 20 | "paths": [ 21 | "/" 22 | ], 23 | "hostname": "host2", 24 | "username": "root", 25 | "tags": [ 26 | "tag1" 27 | ], 28 | "id": "96a3130fa1ec193aa19d80e1518adfe1424e382944249fe06e7aeca207de2753", 29 | "short_id": "96a3130f" 30 | }, 31 | { 32 | "time": "2020-04-03T01:00:01.361818086+02:00", 33 | "parent": "96a3130fa1ec193aa19d80e1518adfe1424e382944249fe06e7aeca207de2753", 34 | "tree": "872d29ab20f0c8def1b90ba92cb2ef9f58928104bd3dc4751829e73fed9eb44d", 35 | "paths": [ 36 | "/" 37 | ], 38 | "hostname": "host1", 39 | "username": "root", 40 | "tags": [ 41 | "tag1" 42 | ], 43 | "id": "377c6681cd947a729b1f9fd721bf49ef0c7d2a46a1f81ce54024f5c8a71e2de8", 44 | "short_id": "377c6681" 45 | }, 46 | { 47 | "time": "2020-04-16T01:00:01.462111488+02:00", 48 | "parent": "377c6681cd947a729b1f9fd721bf49ef0c7d2a46a1f81ce54024f5c8a71e2de8", 49 | "tree": "0b54dc511d57bdee1022632ba9e0da4303441bb76014d77c2db2b0784d3cafbb", 50 | "paths": [ 51 | "/backup" 52 | ], 53 | "hostname": "host1", 54 | "username": "root", 55 | "tags": [ 56 | "tag1" 57 | ], 58 | "id": "b7dc3890fc0bf14229f456fa521b61e5bda7bd819e91a0ccde964d4ce099917c", 59 | "short_id": "b7dc3890" 60 | }, 61 | { 62 | "time": "2020-04-17T01:00:01.071142313+02:00", 63 | "parent": "b7dc3890fc0bf14229f456fa521b61e5bda7bd819e91a0ccde964d4ce099917c", 64 | "tree": "7453f14904daee526e7ae471f63042050098c54e0beb091131c166de346cdab7", 65 | "paths": [ 66 | "/" 67 | ], 68 | "hostname": "host1", 69 | "username": "root", 70 | "tags": [ 71 | "tag1", 72 | "tag2" 73 | ], 74 | "id": "960f57171987ffe66a9481e4f48a00d5c2b743498943df3007c52c30ebb5c013", 75 | "short_id": "960f5717" 76 | }, 77 | { 78 | "time": "2020-04-18T01:00:01.758490768+02:00", 79 | "parent": "960f57171987ffe66a9481e4f48a00d5c2b743498943df3007c52c30ebb5c013", 80 | "tree": "5d54078a97020e917fdcf8d57db877a56887b6cdc30505e7a13881c80c6d0feb", 81 | "paths": [ 82 | "/", 83 | "/backup" 84 | ], 85 | "hostname": "host1", 86 | "username": "root", 87 | "tags": [], 88 | "id": "831ae781821f9999447f1d14412f37b26286f37a69ac89299bda84b9a258c502", 89 | "short_id": "831ae781" 90 | }, 91 | { 92 | "time": "2020-04-19T01:00:01.984942933+02:00", 93 | "parent": "831ae781821f9999447f1d14412f37b26286f37a69ac89299bda84b9a258c502", 94 | "tree": "1a1356add3e8fee3137180fee503ecbc278c63e12c5862cc2d853a82d0ef95ad", 95 | "paths": [ 96 | "/", 97 | "/backup" 98 | ], 99 | "hostname": "host2", 100 | "username": "root", 101 | "tags": [ 102 | "tag2", 103 | "tag1" 104 | ], 105 | "id": "aa20a986662c2e2e9dbed63b56d422b5bc286545a6b28e98e29b47a4f0576065", 106 | "short_id": "aa20a986" 107 | }, 108 | { 109 | "time": "2020-04-20T01:00:01.313020381+02:00", 110 | "parent": "aa20a986662c2e2e9dbed63b56d422b5bc286545a6b28e98e29b47a4f0576065", 111 | "tree": "1fff995bcc6ce0ff17edfa648904cc07d3440bc45f919611a693bfd9bee33018", 112 | "paths": [ 113 | "/" 114 | ], 115 | "hostname": "host1", 116 | "username": "root", 117 | "tags": [ 118 | "tag1" 119 | ], 120 | "id": "f80b2121fd861deb04ae475fbc9b4119fd18986ce1c72e483b4f0dd64b022a8f", 121 | "short_id": "f80b2121" 122 | }, 123 | { 124 | "time": "2020-04-21T01:00:02.019932088+02:00", 125 | "parent": "f80b2121fd861deb04ae475fbc9b4119fd18986ce1c72e483b4f0dd64b022a8f", 126 | "tree": "130036dadb4dafe833a04acf78b0f9fb7024d740f8d761736959d4275a95e404", 127 | "paths": [ 128 | "/" 129 | ], 130 | "hostname": "host2", 131 | "username": "root", 132 | "tags": [ 133 | "tag2" 134 | ], 135 | "id": "11deef410e0400809377277a8bdcd8eb22d0e1e88ad0f9b08e90c751acb167c0", 136 | "short_id": "11deef41" 137 | }, 138 | { 139 | "time": "2020-04-22T01:00:01.167695227+02:00", 140 | "parent": "11deef410e0400809377277a8bdcd8eb22d0e1e88ad0f9b08e90c751acb167c0", 141 | "tree": "6c0e1c996ab6403f348c5e6b8861d8a2e9e9328e43653d364c7f2838de3b4ec6", 142 | "paths": [ 143 | "/" 144 | ], 145 | "hostname": "host1", 146 | "username": "root", 147 | "tags": [ 148 | "tag1" 149 | ], 150 | "id": "9ac89c9bed1fd8f04ed876969a831bb6dfe722527f3f114e283d04e8439307be", 151 | "short_id": "9ac89c9b" 152 | }, 153 | { 154 | "time": "2020-04-23T01:00:01.316636616+02:00", 155 | "parent": "9ac89c9bed1fd8f04ed876969a831bb6dfe722527f3f114e283d04e8439307be", 156 | "tree": "dfc0b7eec7a0c4e22309d6707207c5c03596e5cc74fc382292a229781ee29631", 157 | "paths": [ 158 | "/" 159 | ], 160 | "hostname": "host2", 161 | "username": "root", 162 | "tags": [ 163 | "tag1" 164 | ], 165 | "id": "a8fa6e458b977150d16bab1a49f9737df1172c335a585aeb5b7772d9d19c175e", 166 | "short_id": "a8fa6e45" 167 | }, 168 | { 169 | "time": "2020-04-24T01:00:01.342704178+02:00", 170 | "parent": "a8fa6e458b977150d16bab1a49f9737df1172c335a585aeb5b7772d9d19c175e", 171 | "tree": "98cb91bda16ed99ebde8f2e11a9afe8d70885772a63cc2a3d650abf3666058b5", 172 | "paths": [ 173 | "/" 174 | ], 175 | "hostname": "host1", 176 | "username": "root", 177 | "tags": [ 178 | "tag1" 179 | ], 180 | "id": "f26051fcdcb25e6b5059f66e10999ab12984023bc804df0692a6d80ecf8e4635", 181 | "short_id": "f26051fc" 182 | }, 183 | { 184 | "time": "2020-04-25T01:00:01.748135778+02:00", 185 | "parent": "f26051fcdcb25e6b5059f66e10999ab12984023bc804df0692a6d80ecf8e4635", 186 | "tree": "40870c0fcf7adea468a293664ed55ab3a3b37d97e67c139376a02f7d7d42bb55", 187 | "paths": [ 188 | "/" 189 | ], 190 | "hostname": "host2", 191 | "username": "root", 192 | "tags": [ 193 | "tag2" 194 | ], 195 | "id": "917155736ba2db0c5dc70df13801eeb56b79ebf351c3937753d674569c0a7723", 196 | "short_id": "91715573" 197 | }, 198 | { 199 | "time": "2020-04-26T01:00:01.37224266+02:00", 200 | "parent": "917155736ba2db0c5dc70df13801eeb56b79ebf351c3937753d674569c0a7723", 201 | "tree": "798dffb5a43daca0a5bde43cd023a88216cc22ae9cc71bfec831b3d945366500", 202 | "paths": [ 203 | "/" 204 | ], 205 | "hostname": "host2", 206 | "username": "root", 207 | "tags": [ 208 | "tag2" 209 | ], 210 | "id": "4f1bae37f837865a592b33af5fec17717518d9f975a0fc3f7a475b918eaf65c7", 211 | "short_id": "4f1bae37" 212 | }, 213 | { 214 | "time": "2020-04-27T01:00:01.837431029+02:00", 215 | "parent": "4f1bae37f837865a592b33af5fec17717518d9f975a0fc3f7a475b918eaf65c7", 216 | "tree": "174b3f5ff666cf5de2af865f8abff7459f8885002cb1269b0b928b871892836d", 217 | "paths": [ 218 | "/" 219 | ], 220 | "hostname": "host2", 221 | "username": "root", 222 | "tags": [ 223 | "tag2" 224 | ], 225 | "id": "7cf03eb2897c541ceb5e2e6f1b946338d2f892db03ce7d0687f8cf87bcb044cf", 226 | "short_id": "7cf03eb2" 227 | }, 228 | { 229 | "time": "2020-04-28T01:00:01.343905016+02:00", 230 | "parent": "7cf03eb2897c541ceb5e2e6f1b946338d2f892db03ce7d0687f8cf87bcb044cf", 231 | "tree": "692d841984656ceb47bc1ae4e36e54f18119be8d4a915a6212172ed24ac93cc2", 232 | "paths": [ 233 | "/" 234 | ], 235 | "hostname": "host2", 236 | "username": "root", 237 | "tags": [ 238 | "tag2" 239 | ], 240 | "id": "e802b0a71501e7d19a9950dfa29789a4baf3079f88f04321f67fd524f7abb8e8", 241 | "short_id": "e802b0a7" 242 | }, 243 | { 244 | "time": "2020-04-29T01:00:01.694216182+02:00", 245 | "parent": "e802b0a71501e7d19a9950dfa29789a4baf3079f88f04321f67fd524f7abb8e8", 246 | "tree": "8397658f5281a927c70380534645f8a3115b8f14269932535e238f07b92fd497", 247 | "paths": [ 248 | "/" 249 | ], 250 | "hostname": "host2", 251 | "username": "root", 252 | "tags": [ 253 | "tag2" 254 | ], 255 | "id": "759866ce9357cee87cdfeab27768b6e96ace7536b5d3f788dd0692663dd4d3f8", 256 | "short_id": "759866ce" 257 | }, 258 | { 259 | "time": "2020-04-30T01:00:01.664171782+02:00", 260 | "parent": "759866ce9357cee87cdfeab27768b6e96ace7536b5d3f788dd0692663dd4d3f8", 261 | "tree": "746423d59efbdc3da5b2e6d56cf7214953f22ddff757b34f02cc37a6e2737e87", 262 | "paths": [ 263 | "/" 264 | ], 265 | "hostname": "host2", 266 | "username": "root", 267 | "tags": [ 268 | "tag2" 269 | ], 270 | "id": "50501e5c3998432c9f27fdeb1073f6a5643f51592eda19ebdad7632feab056c0", 271 | "short_id": "50501e5c" 272 | } 273 | ] -------------------------------------------------------------------------------- /examples/grafana.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "6.7.3" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "graph", 22 | "name": "Graph", 23 | "version": "" 24 | }, 25 | { 26 | "type": "datasource", 27 | "id": "prometheus", 28 | "name": "Prometheus", 29 | "version": "1.0.0" 30 | }, 31 | { 32 | "type": "panel", 33 | "id": "stat", 34 | "name": "Stat", 35 | "version": "" 36 | }, 37 | { 38 | "type": "panel", 39 | "id": "table", 40 | "name": "Table", 41 | "version": "" 42 | } 43 | ], 44 | "annotations": { 45 | "list": [ 46 | { 47 | "builtIn": 1, 48 | "datasource": "-- Grafana --", 49 | "enable": true, 50 | "hide": true, 51 | "iconColor": "rgba(0, 211, 255, 1)", 52 | "name": "Annotations & Alerts", 53 | "type": "dashboard" 54 | } 55 | ] 56 | }, 57 | "editable": true, 58 | "gnetId": null, 59 | "graphTooltip": 0, 60 | "id": null, 61 | "links": [], 62 | "panels": [ 63 | { 64 | "datasource": "${DS_PROMETHEUS}", 65 | "gridPos": { 66 | "h": 3, 67 | "w": 24, 68 | "x": 0, 69 | "y": 0 70 | }, 71 | "id": 7, 72 | "options": { 73 | "colorMode": "background", 74 | "fieldOptions": { 75 | "calcs": [ 76 | "last" 77 | ], 78 | "defaults": { 79 | "mappings": [ 80 | { 81 | "from": "", 82 | "id": 1, 83 | "operator": "", 84 | "text": "OK", 85 | "to": "", 86 | "type": 1, 87 | "value": "1" 88 | }, 89 | { 90 | "from": "", 91 | "id": 2, 92 | "operator": "", 93 | "text": "ERR", 94 | "to": "", 95 | "type": 1, 96 | "value": "0" 97 | }, 98 | { 99 | "from": "", 100 | "id": 3, 101 | "operator": "", 102 | "text": "Unknown", 103 | "to": "", 104 | "type": 1, 105 | "value": "null" 106 | } 107 | ], 108 | "thresholds": { 109 | "mode": "absolute", 110 | "steps": [ 111 | { 112 | "color": "red", 113 | "value": null 114 | }, 115 | { 116 | "color": "green", 117 | "value": 0.5 118 | } 119 | ] 120 | }, 121 | "title": "" 122 | }, 123 | "overrides": [], 124 | "values": false 125 | }, 126 | "graphMode": "none", 127 | "justifyMode": "auto", 128 | "orientation": "vertical" 129 | }, 130 | "pluginVersion": "6.7.3", 131 | "targets": [ 132 | { 133 | "expr": "restic_repo_integrity_status", 134 | "instant": false, 135 | "interval": "", 136 | "legendFormat": "{{ repository }}", 137 | "refId": "A" 138 | } 139 | ], 140 | "timeFrom": null, 141 | "timeShift": null, 142 | "title": "Repositories integrity", 143 | "type": "stat" 144 | }, 145 | { 146 | "datasource": "${DS_PROMETHEUS}", 147 | "gridPos": { 148 | "h": 2, 149 | "w": 24, 150 | "x": 0, 151 | "y": 3 152 | }, 153 | "id": 8, 154 | "options": { 155 | "colorMode": "value", 156 | "fieldOptions": { 157 | "calcs": [ 158 | "last" 159 | ], 160 | "defaults": { 161 | "mappings": [], 162 | "thresholds": { 163 | "mode": "absolute", 164 | "steps": [ 165 | { 166 | "color": "blue", 167 | "value": null 168 | } 169 | ] 170 | }, 171 | "title": "", 172 | "unit": "dateTimeFromNow" 173 | }, 174 | "overrides": [], 175 | "values": false 176 | }, 177 | "graphMode": "none", 178 | "justifyMode": "auto", 179 | "orientation": "vertical" 180 | }, 181 | "pluginVersion": "6.7.3", 182 | "targets": [ 183 | { 184 | "expr": "restic_repo_integrity_status_latest_seconds * 1000", 185 | "instant": false, 186 | "interval": "", 187 | "legendFormat": "{{ repository }}", 188 | "refId": "A" 189 | } 190 | ], 191 | "timeFrom": null, 192 | "timeShift": null, 193 | "title": "", 194 | "transparent": true, 195 | "type": "stat" 196 | }, 197 | { 198 | "aliasColors": {}, 199 | "bars": false, 200 | "dashLength": 10, 201 | "dashes": false, 202 | "datasource": "${DS_PROMETHEUS}", 203 | "decimals": 0, 204 | "fill": 1, 205 | "fillGradient": 0, 206 | "gridPos": { 207 | "h": 8, 208 | "w": 12, 209 | "x": 0, 210 | "y": 5 211 | }, 212 | "hiddenSeries": false, 213 | "id": 2, 214 | "legend": { 215 | "alignAsTable": true, 216 | "avg": false, 217 | "current": true, 218 | "hideEmpty": false, 219 | "hideZero": false, 220 | "max": false, 221 | "min": false, 222 | "rightSide": false, 223 | "show": true, 224 | "total": false, 225 | "values": true 226 | }, 227 | "lines": true, 228 | "linewidth": 1, 229 | "nullPointMode": "null", 230 | "options": { 231 | "dataLinks": [] 232 | }, 233 | "percentage": false, 234 | "pointradius": 2, 235 | "points": false, 236 | "renderer": "flot", 237 | "seriesOverrides": [], 238 | "spaceLength": 10, 239 | "stack": false, 240 | "steppedLine": false, 241 | "targets": [ 242 | { 243 | "expr": "sum by (repository) (restic_repo_snapshots_total)", 244 | "format": "time_series", 245 | "instant": false, 246 | "legendFormat": "{{ repository }}", 247 | "refId": "A" 248 | } 249 | ], 250 | "thresholds": [], 251 | "timeFrom": null, 252 | "timeRegions": [], 253 | "timeShift": null, 254 | "title": "Total number of snapshots per repository", 255 | "tooltip": { 256 | "shared": true, 257 | "sort": 0, 258 | "value_type": "individual" 259 | }, 260 | "type": "graph", 261 | "xaxis": { 262 | "buckets": null, 263 | "mode": "time", 264 | "name": null, 265 | "show": true, 266 | "values": [] 267 | }, 268 | "yaxes": [ 269 | { 270 | "decimals": 0, 271 | "format": "short", 272 | "label": "snapshot count ", 273 | "logBase": 1, 274 | "max": null, 275 | "min": null, 276 | "show": true 277 | }, 278 | { 279 | "format": "short", 280 | "label": null, 281 | "logBase": 1, 282 | "max": null, 283 | "min": null, 284 | "show": true 285 | } 286 | ], 287 | "yaxis": { 288 | "align": false, 289 | "alignLevel": null 290 | } 291 | }, 292 | { 293 | "aliasColors": {}, 294 | "bars": false, 295 | "dashLength": 10, 296 | "dashes": false, 297 | "datasource": "${DS_PROMETHEUS}", 298 | "decimals": 0, 299 | "description": "", 300 | "fill": 1, 301 | "fillGradient": 0, 302 | "gridPos": { 303 | "h": 8, 304 | "w": 12, 305 | "x": 12, 306 | "y": 5 307 | }, 308 | "hiddenSeries": false, 309 | "id": 3, 310 | "legend": { 311 | "alignAsTable": true, 312 | "avg": false, 313 | "current": true, 314 | "hideEmpty": false, 315 | "hideZero": false, 316 | "max": false, 317 | "min": false, 318 | "rightSide": false, 319 | "show": true, 320 | "total": false, 321 | "values": true 322 | }, 323 | "lines": true, 324 | "linewidth": 1, 325 | "nullPointMode": "null", 326 | "options": { 327 | "dataLinks": [] 328 | }, 329 | "percentage": false, 330 | "pointradius": 2, 331 | "points": false, 332 | "renderer": "flot", 333 | "seriesOverrides": [], 334 | "spaceLength": 10, 335 | "stack": false, 336 | "steppedLine": false, 337 | "targets": [ 338 | { 339 | "expr": "sum by (repository, host, paths) (restic_group_snapshots_total)", 340 | "format": "time_series", 341 | "instant": false, 342 | "intervalFactor": 1, 343 | "refId": "A" 344 | } 345 | ], 346 | "thresholds": [], 347 | "timeFrom": null, 348 | "timeRegions": [], 349 | "timeShift": null, 350 | "title": "Total number of snapshots per group", 351 | "tooltip": { 352 | "shared": true, 353 | "sort": 0, 354 | "value_type": "individual" 355 | }, 356 | "type": "graph", 357 | "xaxis": { 358 | "buckets": null, 359 | "mode": "time", 360 | "name": null, 361 | "show": true, 362 | "values": [] 363 | }, 364 | "yaxes": [ 365 | { 366 | "decimals": 0, 367 | "format": "short", 368 | "label": "snapshot count ", 369 | "logBase": 1, 370 | "max": null, 371 | "min": null, 372 | "show": true 373 | }, 374 | { 375 | "format": "short", 376 | "label": null, 377 | "logBase": 1, 378 | "max": null, 379 | "min": null, 380 | "show": true 381 | } 382 | ], 383 | "yaxis": { 384 | "align": false, 385 | "alignLevel": null 386 | } 387 | }, 388 | { 389 | "cacheTimeout": null, 390 | "columns": [], 391 | "datasource": "${DS_PROMETHEUS}", 392 | "fontSize": "100%", 393 | "gridPos": { 394 | "h": 5, 395 | "w": 24, 396 | "x": 0, 397 | "y": 13 398 | }, 399 | "id": 5, 400 | "links": [], 401 | "pageSize": null, 402 | "pluginVersion": "6.4.3", 403 | "showHeader": true, 404 | "sort": { 405 | "col": 0, 406 | "desc": true 407 | }, 408 | "styles": [ 409 | { 410 | "alias": "Date", 411 | "align": "auto", 412 | "colorMode": null, 413 | "colors": [ 414 | "rgba(50, 172, 45, 0.97)", 415 | "rgba(237, 129, 40, 0.89)", 416 | "rgba(245, 54, 54, 0.9)" 417 | ], 418 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 419 | "decimals": null, 420 | "mappingType": 1, 421 | "pattern": "Value #B", 422 | "thresholds": [ 423 | "" 424 | ], 425 | "type": "number", 426 | "unit": "dateTimeAsIso" 427 | }, 428 | { 429 | "alias": "From now", 430 | "align": "auto", 431 | "colorMode": "value", 432 | "colors": [ 433 | "rgba(50, 172, 45, 0.97)", 434 | "rgba(237, 129, 40, 0.89)", 435 | "rgba(245, 54, 54, 0.9)" 436 | ], 437 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 438 | "decimals": 1, 439 | "mappingType": 1, 440 | "pattern": "Value #C", 441 | "thresholds": [ 442 | "86400", 443 | "90000" 444 | ], 445 | "type": "number", 446 | "unit": "dtdurations" 447 | }, 448 | { 449 | "alias": "", 450 | "align": "auto", 451 | "colorMode": null, 452 | "colors": [ 453 | "rgba(245, 54, 54, 0.9)", 454 | "rgba(237, 129, 40, 0.89)", 455 | "rgba(50, 172, 45, 0.97)" 456 | ], 457 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 458 | "decimals": 2, 459 | "mappingType": 1, 460 | "pattern": "/Time|instance|job/", 461 | "thresholds": [], 462 | "type": "hidden", 463 | "unit": "short" 464 | } 465 | ], 466 | "targets": [ 467 | { 468 | "expr": "restic_group_snapshot_latest_seconds * 1000", 469 | "format": "table", 470 | "instant": true, 471 | "interval": "", 472 | "legendFormat": "", 473 | "refId": "B" 474 | }, 475 | { 476 | "expr": "time() - (restic_group_snapshot_latest_seconds)", 477 | "format": "table", 478 | "instant": true, 479 | "interval": "", 480 | "legendFormat": "", 481 | "refId": "C" 482 | } 483 | ], 484 | "timeFrom": null, 485 | "timeShift": null, 486 | "title": "Latest snapshots", 487 | "transform": "table", 488 | "type": "table" 489 | }, 490 | { 491 | "cacheTimeout": null, 492 | "columns": [], 493 | "datasource": "${DS_PROMETHEUS}", 494 | "fontSize": "100%", 495 | "gridPos": { 496 | "h": 5, 497 | "w": 24, 498 | "x": 0, 499 | "y": 18 500 | }, 501 | "id": 9, 502 | "links": [], 503 | "pageSize": null, 504 | "pluginVersion": "6.4.3", 505 | "showHeader": true, 506 | "sort": { 507 | "col": 0, 508 | "desc": true 509 | }, 510 | "styles": [ 511 | { 512 | "alias": "Date", 513 | "align": "auto", 514 | "colorMode": null, 515 | "colors": [ 516 | "rgba(50, 172, 45, 0.97)", 517 | "rgba(237, 129, 40, 0.89)", 518 | "rgba(245, 54, 54, 0.9)" 519 | ], 520 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 521 | "decimals": null, 522 | "mappingType": 1, 523 | "pattern": "Value #B", 524 | "thresholds": [ 525 | "" 526 | ], 527 | "type": "number", 528 | "unit": "dateTimeAsIso" 529 | }, 530 | { 531 | "alias": "From now", 532 | "align": "auto", 533 | "colorMode": "value", 534 | "colors": [ 535 | "rgba(50, 172, 45, 0.97)", 536 | "rgba(237, 129, 40, 0.89)", 537 | "rgba(245, 54, 54, 0.9)" 538 | ], 539 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 540 | "decimals": 1, 541 | "mappingType": 1, 542 | "pattern": "Value #C", 543 | "thresholds": [ 544 | "86400", 545 | "90000" 546 | ], 547 | "type": "number", 548 | "unit": "dtdurations" 549 | }, 550 | { 551 | "alias": "", 552 | "align": "auto", 553 | "colorMode": null, 554 | "colors": [ 555 | "rgba(245, 54, 54, 0.9)", 556 | "rgba(237, 129, 40, 0.89)", 557 | "rgba(50, 172, 45, 0.97)" 558 | ], 559 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 560 | "decimals": 2, 561 | "mappingType": 1, 562 | "pattern": "/Time|instance|job/", 563 | "thresholds": [], 564 | "type": "hidden", 565 | "unit": "short" 566 | } 567 | ], 568 | "targets": [ 569 | { 570 | "expr": "restic_repo_retention_forget_latest_seconds * 1000", 571 | "format": "table", 572 | "instant": true, 573 | "interval": "", 574 | "legendFormat": "", 575 | "refId": "B" 576 | }, 577 | { 578 | "expr": "time() - (restic_repo_retention_forget_latest_seconds)", 579 | "format": "table", 580 | "instant": true, 581 | "interval": "", 582 | "legendFormat": "", 583 | "refId": "C" 584 | } 585 | ], 586 | "timeFrom": null, 587 | "timeShift": null, 588 | "title": "Retention policy last applies", 589 | "transform": "table", 590 | "type": "table" 591 | }, 592 | { 593 | "cacheTimeout": null, 594 | "columns": [], 595 | "datasource": "${DS_PROMETHEUS}", 596 | "fontSize": "100%", 597 | "gridPos": { 598 | "h": 13, 599 | "w": 24, 600 | "x": 0, 601 | "y": 23 602 | }, 603 | "id": 10, 604 | "links": [], 605 | "pageSize": 10, 606 | "pluginVersion": "6.4.3", 607 | "showHeader": true, 608 | "sort": { 609 | "col": 0, 610 | "desc": true 611 | }, 612 | "styles": [ 613 | { 614 | "alias": "Date", 615 | "align": "auto", 616 | "colorMode": null, 617 | "colors": [ 618 | "rgba(50, 172, 45, 0.97)", 619 | "rgba(237, 129, 40, 0.89)", 620 | "rgba(245, 54, 54, 0.9)" 621 | ], 622 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 623 | "decimals": null, 624 | "mappingType": 1, 625 | "pattern": "Value", 626 | "thresholds": [ 627 | "" 628 | ], 629 | "type": "number", 630 | "unit": "dateTimeAsIso" 631 | }, 632 | { 633 | "alias": "", 634 | "align": "auto", 635 | "colorMode": null, 636 | "colors": [ 637 | "rgba(245, 54, 54, 0.9)", 638 | "rgba(237, 129, 40, 0.89)", 639 | "rgba(50, 172, 45, 0.97)" 640 | ], 641 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 642 | "decimals": 2, 643 | "mappingType": 1, 644 | "pattern": "/Time|instance|job|__name__/", 645 | "thresholds": [], 646 | "type": "hidden", 647 | "unit": "short" 648 | } 649 | ], 650 | "targets": [ 651 | { 652 | "expr": "sort(restic_repo_snapshot_datetime_seconds * 1000)", 653 | "format": "table", 654 | "instant": true, 655 | "interval": "", 656 | "intervalFactor": 1, 657 | "legendFormat": "", 658 | "refId": "B" 659 | } 660 | ], 661 | "timeFrom": null, 662 | "timeShift": null, 663 | "title": "All snapshots list", 664 | "transform": "table", 665 | "type": "table" 666 | }, 667 | { 668 | "aliasColors": {}, 669 | "bars": false, 670 | "dashLength": 10, 671 | "dashes": false, 672 | "datasource": "${DS_PROMETHEUS}", 673 | "fill": 1, 674 | "fillGradient": 0, 675 | "gridPos": { 676 | "h": 8, 677 | "w": 24, 678 | "x": 0, 679 | "y": 36 680 | }, 681 | "hiddenSeries": false, 682 | "id": 12, 683 | "legend": { 684 | "alignAsTable": true, 685 | "avg": true, 686 | "current": false, 687 | "max": true, 688 | "min": true, 689 | "show": true, 690 | "total": false, 691 | "values": true 692 | }, 693 | "lines": true, 694 | "linewidth": 1, 695 | "nullPointMode": "null", 696 | "options": { 697 | "dataLinks": [] 698 | }, 699 | "percentage": false, 700 | "pointradius": 2, 701 | "points": false, 702 | "renderer": "flot", 703 | "seriesOverrides": [], 704 | "spaceLength": 10, 705 | "stack": false, 706 | "steppedLine": false, 707 | "targets": [ 708 | { 709 | "expr": "restic_scrape_duration_seconds", 710 | "interval": "", 711 | "legendFormat": "", 712 | "refId": "A" 713 | } 714 | ], 715 | "thresholds": [], 716 | "timeFrom": null, 717 | "timeRegions": [], 718 | "timeShift": null, 719 | "title": "Scrape duration", 720 | "tooltip": { 721 | "shared": true, 722 | "sort": 0, 723 | "value_type": "individual" 724 | }, 725 | "type": "graph", 726 | "xaxis": { 727 | "buckets": null, 728 | "mode": "time", 729 | "name": null, 730 | "show": true, 731 | "values": [] 732 | }, 733 | "yaxes": [ 734 | { 735 | "format": "dtdurations", 736 | "label": "duration", 737 | "logBase": 1, 738 | "max": null, 739 | "min": null, 740 | "show": true 741 | }, 742 | { 743 | "format": "short", 744 | "label": null, 745 | "logBase": 1, 746 | "max": null, 747 | "min": null, 748 | "show": true 749 | } 750 | ], 751 | "yaxis": { 752 | "align": false, 753 | "alignLevel": null 754 | } 755 | } 756 | ], 757 | "refresh": "5s", 758 | "schemaVersion": 22, 759 | "style": "dark", 760 | "tags": [], 761 | "templating": { 762 | "list": [] 763 | }, 764 | "time": { 765 | "from": "now-6h", 766 | "to": "now" 767 | }, 768 | "timepicker": { 769 | "refresh_intervals": [ 770 | "5s", 771 | "10s", 772 | "30s", 773 | "1m", 774 | "5m", 775 | "15m", 776 | "30m", 777 | "1h", 778 | "2h", 779 | "1d" 780 | ] 781 | }, 782 | "timezone": "", 783 | "title": "Restic Controller", 784 | "uid": "PZxP2gRGk", 785 | "variables": { 786 | "list": [] 787 | }, 788 | "version": 16 789 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 42 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 43 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 44 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 45 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 46 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 47 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 48 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 49 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 50 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 51 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 52 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 53 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 54 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 56 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 58 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 59 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 60 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 61 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 62 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 63 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 64 | github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 65 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 66 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 67 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 68 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 69 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 70 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 71 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 72 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 73 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 74 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 75 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 76 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 77 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 78 | github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= 79 | github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 80 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 81 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 82 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 83 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 84 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 85 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 86 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 87 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 88 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 89 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 90 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 91 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 92 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 93 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 94 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 95 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 96 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 97 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 98 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 99 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 100 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 101 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 102 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 103 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 104 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 105 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 106 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 107 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 108 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 109 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 110 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 111 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 112 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 113 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 115 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 116 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 117 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 118 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 119 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 120 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 121 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 122 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 123 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 124 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 125 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 126 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 127 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 128 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 129 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 130 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 131 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 132 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 133 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 134 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 135 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 136 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 137 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 138 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 139 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 140 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 141 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 142 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 143 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 144 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 145 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 146 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 147 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 148 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 149 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 150 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 151 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 152 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 153 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 154 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 155 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 156 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 157 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 158 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 159 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 160 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 161 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 162 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 163 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 164 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 165 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 166 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 167 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 168 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 169 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 170 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 171 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 172 | github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 173 | github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 174 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 175 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 176 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 177 | github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 178 | github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 179 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 180 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 181 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 182 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 183 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 184 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 185 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 186 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 187 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 188 | github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= 189 | github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 190 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 191 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 192 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 193 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 194 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 195 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 196 | github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= 197 | github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= 198 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 199 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 200 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 201 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 202 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 203 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 204 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 205 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 206 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 207 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 208 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 209 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 210 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 211 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 212 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 213 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 214 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 215 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 216 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 217 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 218 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 219 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 220 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 221 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 222 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 223 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 224 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 225 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 226 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 227 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 228 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 229 | golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= 230 | golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 231 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 232 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 233 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 234 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 235 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 236 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 237 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 238 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 239 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 240 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 241 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 242 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 243 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 244 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 245 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 246 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 247 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 248 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 249 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 250 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 251 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 252 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 253 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 254 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 255 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 256 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 257 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 258 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 259 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 260 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 261 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 262 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 263 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 264 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 265 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 266 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 267 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 268 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 269 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 270 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 271 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 272 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 273 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 274 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 275 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 276 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 277 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 278 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 279 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 280 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 281 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 282 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 283 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 284 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 285 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 286 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 287 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 288 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 289 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 290 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 291 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 292 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 293 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 294 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 295 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 296 | golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= 297 | golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 298 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 299 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 300 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 301 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 302 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 303 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 304 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 305 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 306 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 307 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 308 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 309 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 310 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 311 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 312 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 313 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 314 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 315 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 316 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 317 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 318 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 319 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 320 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 321 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 322 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 325 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 327 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 328 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 329 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 352 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 353 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 354 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 355 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 356 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 357 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 358 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 359 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 360 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 361 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 362 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 363 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 364 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 365 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 366 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 367 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 368 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 369 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 370 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 371 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 372 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 373 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 374 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 375 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 376 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 377 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 378 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 379 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 380 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 381 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 382 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 383 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 384 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 385 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 386 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 387 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 388 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 389 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 390 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 391 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 392 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 393 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 394 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 395 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 396 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 397 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 398 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 399 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 400 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 401 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 402 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 403 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 404 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 405 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 406 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 407 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 408 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 409 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 410 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 411 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 412 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 413 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 414 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 415 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 416 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 417 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 418 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 419 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 420 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 421 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 422 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 423 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 424 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 425 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 426 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 427 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 428 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 429 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 430 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 431 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 432 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 433 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 434 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 435 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 436 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 437 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 438 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 439 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 440 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 441 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 442 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 443 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 444 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 445 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 446 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 447 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 448 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 449 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 450 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 451 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 452 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 453 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 454 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 455 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 456 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 457 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 458 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 459 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 460 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 461 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 462 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 463 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 464 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 465 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 466 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 467 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 468 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 469 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 470 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 471 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 472 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 473 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 474 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 475 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 476 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 477 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 478 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 479 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 480 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 481 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 482 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 483 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 484 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 485 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 486 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 487 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 488 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 489 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 490 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 491 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 492 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 493 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 494 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 495 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 496 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 497 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 498 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 499 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 500 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 501 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 502 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 503 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 504 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 505 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 506 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 507 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 508 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 509 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 510 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 511 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 512 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 513 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 514 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 515 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 516 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 517 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 518 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 519 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 520 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 521 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 522 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 523 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 524 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 525 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 526 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 527 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 528 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 529 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 530 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 531 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 532 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 533 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 534 | --------------------------------------------------------------------------------