WIP: Refactor entire project #16
38 changed files with 2381 additions and 854 deletions
124
.drone.yml
124
.drone.yml
|
@ -1,124 +0,0 @@
|
|||
kind: pipeline
|
||||
name: build
|
||||
|
||||
steps:
|
||||
- name: gofmt
|
||||
image: golang:1.22.4
|
||||
commands:
|
||||
- gofmt -l -s .
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- name: golangci-linter
|
||||
image: golangci/golangci-lint:v1.59.1
|
||||
commands:
|
||||
- golangci-lint run --enable-all ./...
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- name: vuln-check
|
||||
image: golang:1.22.4
|
||||
commands:
|
||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- govulncheck ./...
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- name: docker
|
||||
image: thegeeklab/drone-docker-buildx
|
||||
privileged: true
|
||||
settings:
|
||||
registry: git.ar21.de
|
||||
username:
|
||||
from_secret: REGISTRY_USER
|
||||
password:
|
||||
from_secret: REGISTRY_PASS
|
||||
repo: git.ar21.de/yolokube/grafana-backuper
|
||||
tags:
|
||||
- latest
|
||||
- ${DRONE_BUILD_NUMBER}
|
||||
platforms:
|
||||
- linux/arm64
|
||||
- linux/amd64
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on:
|
||||
- gofmt
|
||||
- golangci-linter
|
||||
- vuln-check
|
||||
- name: docker-build
|
||||
image: thegeeklab/drone-docker-buildx
|
||||
privileged: true
|
||||
settings:
|
||||
registry: git.ar21.de
|
||||
username:
|
||||
from_secret: REGISTRY_USER
|
||||
password:
|
||||
from_secret: REGISTRY_PASS
|
||||
repo: git.ar21.de/yolokube/grafana-backuper
|
||||
tags:
|
||||
- latest
|
||||
- ${DRONE_BUILD_NUMBER}
|
||||
platforms:
|
||||
- linux/arm64
|
||||
- linux/amd64
|
||||
dry_run: true
|
||||
when:
|
||||
branch:
|
||||
exclude:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on:
|
||||
- gofmt
|
||||
- golangci-linter
|
||||
- vuln-check
|
||||
- name: bump tag in deployment-repo
|
||||
image: git.ar21.de/aaron/kustomize-ci
|
||||
commands:
|
||||
- cd /deployment-repo
|
||||
- git clone https://git.ar21.de/yolokube/grafana-backuper-deployment.git .
|
||||
- cd /deployment-repo/overlay
|
||||
- kustomize edit set image git.ar21.de/yolokube/grafana-backuper=git.ar21.de/yolokube/grafana-backuper:${DRONE_BUILD_NUMBER}
|
||||
volumes:
|
||||
- name: deployment-repo
|
||||
path: /deployment-repo
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on:
|
||||
- docker
|
||||
- name: push new tag to deployment-repo
|
||||
image: appleboy/drone-git-push
|
||||
settings:
|
||||
branch: main
|
||||
remote: ssh://git@git.ar21.de:2222/yolokube/grafana-backuper-deployment.git
|
||||
path: /deployment-repo
|
||||
force: false
|
||||
commit: true
|
||||
commit_message: "GRAFANA-BACKUPER: update image tag to ${DRONE_BUILD_NUMBER} (done automagically via Drone pipeline)"
|
||||
ssh_key:
|
||||
from_secret: GITEA_SSH_KEY
|
||||
volumes:
|
||||
- name: deployment-repo
|
||||
path: /deployment-repo
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on:
|
||||
- bump tag in deployment-repo
|
||||
volumes:
|
||||
- name: deployment-repo
|
||||
temp: {}
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
342
.golangci.yml
Normal file
342
.golangci.yml
Normal file
|
@ -0,0 +1,342 @@
|
|||
run:
|
||||
# Timeout for analysis, e.g. 30s, 5m.
|
||||
# Default: 1m
|
||||
timeout: 5m
|
||||
|
||||
|
||||
# This file contains only configs which differ from defaults.
|
||||
# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
|
||||
linters-settings:
|
||||
cyclop:
|
||||
# The maximal code complexity to report.
|
||||
# Default: 10
|
||||
max-complexity: 30
|
||||
# The maximal average package complexity.
|
||||
# If it's higher than 0.0 (float) the check is enabled
|
||||
# Default: 0.0
|
||||
package-average: 10.0
|
||||
|
||||
errcheck:
|
||||
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
|
||||
# Such cases aren't reported by default.
|
||||
# Default: false
|
||||
check-type-assertions: true
|
||||
|
||||
exhaustive:
|
||||
# Program elements to check for exhaustiveness.
|
||||
# Default: [ switch ]
|
||||
check:
|
||||
- switch
|
||||
- map
|
||||
|
||||
exhaustruct:
|
||||
# List of regular expressions to exclude struct packages and their names from checks.
|
||||
# Regular expressions must match complete canonical struct package/name/structname.
|
||||
# Default: []
|
||||
exclude:
|
||||
# std libs
|
||||
- "^net/http.Client$"
|
||||
- "^net/http.Cookie$"
|
||||
- "^net/http.Request$"
|
||||
- "^net/http.Response$"
|
||||
- "^net/http.Server$"
|
||||
- "^net/http.Transport$"
|
||||
- "^net/url.URL$"
|
||||
- "^os/exec.Cmd$"
|
||||
- "^reflect.StructField$"
|
||||
# public libs
|
||||
- "^github.com/Shopify/sarama.Config$"
|
||||
- "^github.com/Shopify/sarama.ProducerMessage$"
|
||||
- "^github.com/mitchellh/mapstructure.DecoderConfig$"
|
||||
- "^github.com/prometheus/client_golang/.+Opts$"
|
||||
- "^github.com/spf13/cobra.Command$"
|
||||
- "^github.com/spf13/cobra.CompletionOptions$"
|
||||
- "^github.com/stretchr/testify/mock.Mock$"
|
||||
- "^github.com/testcontainers/testcontainers-go.+Request$"
|
||||
- "^github.com/testcontainers/testcontainers-go.FromDockerfile$"
|
||||
- "^golang.org/x/tools/go/analysis.Analyzer$"
|
||||
- "^google.golang.org/protobuf/.+Options$"
|
||||
- "^gopkg.in/yaml.v3.Node$"
|
||||
|
||||
funlen:
|
||||
# Checks the number of lines in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 60
|
||||
lines: 100
|
||||
# Checks the number of statements in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 40
|
||||
statements: 50
|
||||
# Ignore comments when counting lines.
|
||||
# Default false
|
||||
ignore-comments: true
|
||||
|
||||
gocognit:
|
||||
# Minimal code complexity to report.
|
||||
# Default: 30 (but we recommend 10-20)
|
||||
min-complexity: 20
|
||||
|
||||
gocritic:
|
||||
# Settings passed to gocritic.
|
||||
# The settings key is the name of a supported gocritic checker.
|
||||
# The list of supported checkers can be find in https://go-critic.github.io/overview.
|
||||
settings:
|
||||
captLocal:
|
||||
# Whether to restrict checker to params only.
|
||||
# Default: true
|
||||
paramsOnly: false
|
||||
underef:
|
||||
# Whether to skip (*x).method() calls where x is a pointer receiver.
|
||||
# Default: true
|
||||
skipRecvDeref: false
|
||||
|
||||
gomodguard:
|
||||
blocked:
|
||||
# List of blocked modules.
|
||||
# Default: []
|
||||
modules:
|
||||
- github.com/golang/protobuf:
|
||||
recommendations:
|
||||
- google.golang.org/protobuf
|
||||
reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
|
||||
- github.com/satori/go.uuid:
|
||||
recommendations:
|
||||
- github.com/google/uuid
|
||||
reason: "satori's package is not maintained"
|
||||
- github.com/gofrs/uuid:
|
||||
recommendations:
|
||||
- github.com/gofrs/uuid/v5
|
||||
reason: "gofrs' package was not go module before v5"
|
||||
|
||||
govet:
|
||||
# Enable all analyzers.
|
||||
# Default: false
|
||||
enable-all: true
|
||||
# Disable analyzers by name.
|
||||
# Run `go tool vet help` to see all analyzers.
|
||||
# Default: []
|
||||
disable:
|
||||
- fieldalignment # too strict
|
||||
# Settings per analyzer.
|
||||
settings:
|
||||
shadow:
|
||||
# Whether to be strict about shadowing; can be noisy.
|
||||
# Default: false
|
||||
strict: true
|
||||
|
||||
inamedparam:
|
||||
# Skips check for interface methods with only a single parameter.
|
||||
# Default: false
|
||||
skip-single-param: true
|
||||
|
||||
mnd:
|
||||
# List of function patterns to exclude from analysis.
|
||||
# Values always ignored: `time.Date`,
|
||||
# `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
|
||||
# `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
|
||||
# Default: []
|
||||
ignored-functions:
|
||||
- args.Error
|
||||
- flag.Arg
|
||||
- flag.Duration.*
|
||||
- flag.Float.*
|
||||
- flag.Int.*
|
||||
- flag.Uint.*
|
||||
- os.Chmod
|
||||
- os.Mkdir.*
|
||||
- os.OpenFile
|
||||
- os.WriteFile
|
||||
- prometheus.ExponentialBuckets.*
|
||||
- prometheus.LinearBuckets
|
||||
|
||||
nakedret:
|
||||
# Make an issue if func has more lines of code than this setting, and it has naked returns.
|
||||
# Default: 30
|
||||
max-func-lines: 0
|
||||
|
||||
nolintlint:
|
||||
# Exclude following linters from requiring an explanation.
|
||||
# Default: []
|
||||
allow-no-explanation: [ funlen, gocognit, lll ]
|
||||
# Enable to require an explanation of nonzero length after each nolint directive.
|
||||
# Default: false
|
||||
require-explanation: true
|
||||
# Enable to require nolint directives to mention the specific linter being suppressed.
|
||||
# Default: false
|
||||
require-specific: true
|
||||
|
||||
perfsprint:
|
||||
# Optimizes into strings concatenation.
|
||||
# Default: true
|
||||
strconcat: false
|
||||
|
||||
rowserrcheck:
|
||||
# database/sql is always checked
|
||||
# Default: []
|
||||
packages:
|
||||
- github.com/jmoiron/sqlx
|
||||
|
||||
sloglint:
|
||||
# Enforce not using global loggers.
|
||||
# Values:
|
||||
# - "": disabled
|
||||
# - "all": report all global loggers
|
||||
# - "default": report only the default slog logger
|
||||
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global
|
||||
# Default: ""
|
||||
no-global: "all"
|
||||
# Enforce using methods that accept a context.
|
||||
# Values:
|
||||
# - "": disabled
|
||||
# - "all": report all contextless calls
|
||||
# - "scope": report only if a context exists in the scope of the outermost function
|
||||
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only
|
||||
# Default: ""
|
||||
context: "scope"
|
||||
|
||||
tenv:
|
||||
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
|
||||
# Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
|
||||
# Default: false
|
||||
all: true
|
||||
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
## enabled by default
|
||||
- errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
|
||||
- gosimple # specializes in simplifying a code
|
||||
- govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
|
||||
- ineffassign # detects when assignments to existing variables are not used
|
||||
- staticcheck # is a go vet on steroids, applying a ton of static analysis checks
|
||||
- typecheck # like the front-end of a Go compiler, parses and type-checks Go code
|
||||
- unused # checks for unused constants, variables, functions and types
|
||||
## disabled by default
|
||||
- asasalint # checks for pass []any as any in variadic func(...any)
|
||||
- asciicheck # checks that your code does not contain non-ASCII identifiers
|
||||
- bidichk # checks for dangerous unicode character sequences
|
||||
- bodyclose # checks whether HTTP response body is closed successfully
|
||||
- canonicalheader # checks whether net/http.Header uses canonical header
|
||||
- copyloopvar # detects places where loop variables are copied
|
||||
- cyclop # checks function and package cyclomatic complexity
|
||||
- dupl # tool for code clone detection
|
||||
- durationcheck # checks for two durations multiplied together
|
||||
- errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
|
||||
- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
|
||||
- exhaustive # checks exhaustiveness of enum switch statements
|
||||
- exportloopref # checks for pointers to enclosing loop variables
|
||||
- fatcontext # detects nested contexts in loops
|
||||
- forbidigo # forbids identifiers
|
||||
- funlen # tool for detection of long functions
|
||||
- gocheckcompilerdirectives # validates go compiler directive comments (//go:)
|
||||
- gochecknoglobals # checks that no global variables exist
|
||||
- gochecknoinits # checks that no init functions are present in Go code
|
||||
- gochecksumtype # checks exhaustiveness on Go "sum types"
|
||||
- gocognit # computes and checks the cognitive complexity of functions
|
||||
- goconst # finds repeated strings that could be replaced by a constant
|
||||
- gocritic # provides diagnostics that check for bugs, performance and style issues
|
||||
- gocyclo # computes and checks the cyclomatic complexity of functions
|
||||
- godot # checks if comments end in a period
|
||||
- goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
|
||||
- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
|
||||
- gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
|
||||
- goprintffuncname # checks that printf-like functions are named with f at the end
|
||||
- gosec # inspects source code for security problems
|
||||
- intrange # finds places where for loops could make use of an integer range
|
||||
- lll # reports long lines
|
||||
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
|
||||
- makezero # finds slice declarations with non-zero initial length
|
||||
- mirror # reports wrong mirror patterns of bytes/strings usage
|
||||
- misspell # finds commonly misspelled English words in comments
|
||||
- mnd # detects magic numbers
|
||||
- musttag # enforces field tags in (un)marshaled structs
|
||||
- nakedret # finds naked returns in functions greater than a specified function length
|
||||
- nestif # reports deeply nested if statements
|
||||
- nilerr # finds the code that returns nil even if it checks that the error is not nil
|
||||
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
|
||||
- noctx # finds sending http request without context.Context
|
||||
- nolintlint # reports ill-formed or insufficient nolint directives
|
||||
- nonamedreturns # reports all named returns
|
||||
- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
|
||||
- perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
|
||||
- predeclared # finds code that shadows one of Go's predeclared identifiers
|
||||
- promlinter # checks Prometheus metrics naming via promlint
|
||||
- protogetter # reports direct reads from proto message fields when getters should be used
|
||||
- reassign # checks that package variables are not reassigned
|
||||
- revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
|
||||
- rowserrcheck # checks whether Err of rows is checked successfully
|
||||
- sloglint # ensure consistent code style when using log/slog
|
||||
- spancheck # checks for mistakes with OpenTelemetry/Census spans
|
||||
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
|
||||
- stylecheck # is a replacement for golint
|
||||
- tenv # detects using os.Setenv instead of t.Setenv since Go1.17
|
||||
- testableexamples # checks if examples are testable (have an expected output)
|
||||
- testifylint # checks usage of github.com/stretchr/testify
|
||||
- testpackage # makes you use a separate _test package
|
||||
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
|
||||
- unconvert # removes unnecessary type conversions
|
||||
- unparam # reports unused function parameters
|
||||
- usestdlibvars # detects the possibility to use variables/constants from the Go standard library
|
||||
- wastedassign # finds wasted assignment statements
|
||||
- whitespace # detects leading and trailing whitespace
|
||||
|
||||
## you may want to enable
|
||||
#- decorder # checks declaration order and count of types, constants, variables and functions
|
||||
#- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized
|
||||
#- gci # controls golang package import order and makes it always deterministic
|
||||
#- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega
|
||||
#- godox # detects FIXME, TODO and other comment keywords
|
||||
#- goheader # checks is file header matches to pattern
|
||||
#- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters
|
||||
#- interfacebloat # checks the number of methods inside an interface
|
||||
#- ireturn # accept interfaces, return concrete types
|
||||
#- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated
|
||||
#- tagalign # checks that struct tags are well aligned
|
||||
#- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope
|
||||
#- wrapcheck # checks that errors returned from external packages are wrapped
|
||||
#- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event
|
||||
|
||||
## disabled
|
||||
#- containedctx # detects struct contained context.Context field
|
||||
#- contextcheck # [too many false positives] checks the function whether use a non-inherited context
|
||||
#- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages
|
||||
#- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
|
||||
#- dupword # [useless without config] checks for duplicate words in the source code
|
||||
#- err113 # [too strict] checks the errors handling expressions
|
||||
#- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted
|
||||
#- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds
|
||||
#- forcetypeassert # [replaced by errcheck] finds forced type assertions
|
||||
#- gofmt # [replaced by goimports] checks whether code was gofmt-ed
|
||||
#- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed
|
||||
#- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase
|
||||
#- grouper # analyzes expression groups
|
||||
#- importas # enforces consistent import aliases
|
||||
#- maintidx # measures the maintainability index of each function
|
||||
#- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity
|
||||
#- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test
|
||||
#- tagliatelle # checks the struct tags
|
||||
#- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers
|
||||
#- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines
|
||||
|
||||
|
||||
issues:
|
||||
# Maximum count of issues with the same text.
|
||||
# Set to 0 to disable.
|
||||
# Default: 3
|
||||
max-same-issues: 50
|
||||
|
||||
exclude-rules:
|
||||
- source: "(noinspection|TODO)"
|
||||
linters: [ godot ]
|
||||
- source: "//noinspection"
|
||||
linters: [ gocritic ]
|
||||
- path: "_test\\.go"
|
||||
linters:
|
||||
- bodyclose
|
||||
- dupl
|
||||
- funlen
|
||||
- goconst
|
||||
- gosec
|
||||
- noctx
|
||||
- wrapcheck
|
37
.woodpecker/.build.yaml
Normal file
37
.woodpecker/.build.yaml
Normal file
|
@ -0,0 +1,37 @@
|
|||
steps:
|
||||
- name: docker
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: git.ar21.de
|
||||
username:
|
||||
from_secret: REGISTRY_USER
|
||||
password:
|
||||
from_secret: REGISTRY_PASS
|
||||
repo: git.ar21.de/yolokube/grafana-backuper
|
||||
tags:
|
||||
- latest
|
||||
- ${CI_PIPELINE_NUMBER}
|
||||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
- name: docker-staging
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: git.ar21.de
|
||||
username:
|
||||
from_secret: REGISTRY_USER
|
||||
password:
|
||||
from_secret: REGISTRY_PASS
|
||||
repo: git.ar21.de/yolokube/grafana-backuper
|
||||
tags:
|
||||
- staging
|
||||
- staging-${CI_PIPELINE_NUMBER}
|
||||
dry_run: true
|
||||
when:
|
||||
- branch:
|
||||
exclude: main
|
||||
event: push
|
||||
depends_on:
|
||||
- gofmt
|
||||
- lint
|
||||
- vulncheck
|
26
.woodpecker/.deploy.yaml
Normal file
26
.woodpecker/.deploy.yaml
Normal file
|
@ -0,0 +1,26 @@
|
|||
steps:
|
||||
- name: bump tag in deployment-repo
|
||||
image: git.ar21.de/aaron/kustomize-ci
|
||||
commands:
|
||||
- git clone https://git.ar21.de/yolokube/grafana-backuper-deployment.git deployment-repo
|
||||
- cd deployment-repo/overlay
|
||||
- kustomize edit set image git.ar21.de/yolokube/grafana-backuper=git.ar21.de/yolokube/grafana-backuper:${CI_PIPELINE_NUMBER}
|
||||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
- name: push new tag to deployment-repo
|
||||
image: appleboy/drone-git-push
|
||||
settings:
|
||||
branch: main
|
||||
remote: ssh://git@git.ar21.de:2222/yolokube/grafana-backuper-deployment.git
|
||||
path: deployment-repo
|
||||
force: false
|
||||
commit: true
|
||||
commit_message: "GRAFANA-BACKUPER: update image tag to ${CI_PIPELINE_NUMBER} (done automagically via Woodpecker pipeline)"
|
||||
ssh_key:
|
||||
from_secret: FORGEJO_SSH_KEY
|
||||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
depends_on:
|
||||
- build
|
7
.woodpecker/.gofmt.yaml
Normal file
7
.woodpecker/.gofmt.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
steps:
|
||||
- name: gofmt
|
||||
image: golang:1.22.5
|
||||
commands:
|
||||
- gofmt -l -s .
|
||||
when:
|
||||
- event: push
|
7
.woodpecker/.lint.yaml
Normal file
7
.woodpecker/.lint.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
steps:
|
||||
- name: golangci-linter
|
||||
image: golangci/golangci-lint:v1.59.1
|
||||
commands:
|
||||
- golangci-lint run ./...
|
||||
when:
|
||||
- event: push
|
8
.woodpecker/.vulncheck.yaml
Normal file
8
.woodpecker/.vulncheck.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
steps:
|
||||
- name: vuln-check
|
||||
image: golang:1.22.5
|
||||
commands:
|
||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- govulncheck ./...
|
||||
when:
|
||||
- event: push
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.22.4-bookworm AS build
|
||||
FROM golang:1.22.5-bookworm AS build
|
||||
|
||||
# Create build workspace folder
|
||||
WORKDIR /workspace
|
||||
|
@ -9,7 +9,7 @@ RUN apt-get update --yes && \
|
|||
apt-get install --yes build-essential
|
||||
|
||||
# Build the actual binary
|
||||
RUN CGO_ENABLED=0 go build -o grafana-backuper main.go
|
||||
RUN CGO_ENABLED=0 go build -o grafana-backuper cmd/main.go
|
||||
|
||||
# -- -- -- -- -- --
|
||||
|
||||
|
@ -22,4 +22,4 @@ WORKDIR /app
|
|||
# Copy built binary from build image
|
||||
COPY --from=build /workspace/grafana-backuper /app
|
||||
|
||||
ENTRYPOINT ["/app/grafana-backuper"]
|
||||
ENTRYPOINT ["/app/grafana-backuper backup --json"]
|
||||
|
|
91
cfg/cfg.go
91
cfg/cfg.go
|
@ -1,91 +0,0 @@
|
|||
package cfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
)
|
||||
|
||||
type AppSettings struct {
|
||||
Force bool
|
||||
GrafanaURL string
|
||||
GrafanaToken string
|
||||
GitBranch string
|
||||
GitRepoURL string
|
||||
GitUser string
|
||||
GitEmail string
|
||||
GitPass string
|
||||
GPGKey string
|
||||
}
|
||||
|
||||
var cliStruct struct {
|
||||
Force bool `name:"force" short:"f" help:"Force git commits / ignore existence check."`
|
||||
GrafanaURL string `name:"grafana-url" env:"GRAFANA_URL" help:"Grafana URL to access the API"`
|
||||
GrafanaToken string `name:"grafana-auth-token" env:"GRAFANA_AUTH_TOKEN" help:"Grafana auth token to access the API"`
|
||||
GitBranch string `name:"git-branch" env:"GIT_BRANCH" help:"Git branch name" default:"${default_git_branch}"`
|
||||
GitRepoURL string `name:"git-repo-url" env:"GIT_REPO_URL" help:"Complete Git repository URL"`
|
||||
GitUser string `name:"git-user" env:"GIT_USER" help:"Git username"`
|
||||
GitEmail string `name:"git-email" env:"GIT_EMAIL" help:"Git email address"`
|
||||
GitPass string `name:"git-pass" env:"GIT_PASS" help:"Git user password"`
|
||||
GPGKey string `name:"signing-key" env:"GIT_SIGNING_KEY" help:"GPG signing key"`
|
||||
}
|
||||
|
||||
func Parse() *AppSettings {
|
||||
ctx := kong.Parse(
|
||||
&cliStruct,
|
||||
kong.Vars{
|
||||
"default_git_branch": "main",
|
||||
},
|
||||
kong.Name("grafana-backuper"),
|
||||
kong.Description("🚀 CLI tool to convert grafana dashboards to git"),
|
||||
kong.UsageOnError(),
|
||||
)
|
||||
|
||||
validateFlags(ctx)
|
||||
settings := &AppSettings{
|
||||
Force: cliStruct.Force,
|
||||
GrafanaURL: cliStruct.GrafanaURL,
|
||||
GrafanaToken: cliStruct.GrafanaToken,
|
||||
GitBranch: cliStruct.GitBranch,
|
||||
GitRepoURL: cliStruct.GitRepoURL,
|
||||
GitUser: cliStruct.GitUser,
|
||||
GitEmail: cliStruct.GitEmail,
|
||||
GitPass: cliStruct.GitPass,
|
||||
GPGKey: cliStruct.GPGKey,
|
||||
}
|
||||
return settings
|
||||
}
|
||||
|
||||
func validateFlags(cliCtx *kong.Context) {
|
||||
var flagsValid = true
|
||||
var messages = []string{}
|
||||
if cliStruct.GrafanaURL == "" {
|
||||
messages = append(messages, "error: invalid grafana URL, must not be blank")
|
||||
flagsValid = false
|
||||
}
|
||||
if cliStruct.GrafanaToken == "" {
|
||||
messages = append(messages, "error: invalid auth token for grafana, must not be blank")
|
||||
flagsValid = false
|
||||
}
|
||||
if cliStruct.GitRepoURL == "" {
|
||||
messages = append(messages, "error: invalid repo url for git, must not be blank")
|
||||
flagsValid = false
|
||||
}
|
||||
if cliStruct.GitUser == "" {
|
||||
messages = append(messages, "error: invalid username for git, must not be blank")
|
||||
flagsValid = false
|
||||
}
|
||||
if cliStruct.GitPass == "" {
|
||||
messages = append(messages, "error: invalid password for git, must not be blank")
|
||||
flagsValid = false
|
||||
}
|
||||
if !flagsValid {
|
||||
cliCtx.PrintUsage(false)
|
||||
fmt.Println()
|
||||
for i := 0; i < len(messages); i++ {
|
||||
fmt.Println(messages[i])
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
22
cmd/main.go
Normal file
22
cmd/main.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/cmd"
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := &config.Config{}
|
||||
|
||||
rootCmd := cmd.NewRootCommand(cfg)
|
||||
rootCmd.AddCommand(
|
||||
cmd.NewBackupCommand(cfg),
|
||||
cmd.NewRestoreCommand(cfg),
|
||||
)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
44
go.mod
44
go.mod
|
@ -1,32 +1,52 @@
|
|||
module git.ar21.de/yolokube/grafana-backuper
|
||||
|
||||
go 1.22.2
|
||||
go 1.22.5
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v1.0.0
|
||||
github.com/alecthomas/kong v0.9.0
|
||||
github.com/go-git/go-billy/v5 v5.5.0
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.19.0
|
||||
golang.org/x/net v0.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/cloudflare/circl v1.3.9 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/mod v0.15.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
140
go.sum
140
go.sum
|
@ -1,53 +1,53 @@
|
|||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
|
||||
github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
|
||||
github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
|
||||
github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
|
||||
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
|
||||
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
|
||||
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
||||
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
|
||||
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE=
|
||||
github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
||||
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
|
@ -59,46 +59,83 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
||||
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
||||
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
|
||||
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
@ -106,15 +143,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -124,21 +157,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -146,22 +179,23 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
59
internal/cmd/backup.go
Normal file
59
internal/cmd/backup.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/config"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafanabackuper"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewBackupCommand(c *config.Config) *cobra.Command {
|
||||
backupCmd := &cobra.Command{
|
||||
Use: "backup",
|
||||
Short: "Back up the dashboards from grafana to a git repository.",
|
||||
Long: "Back up the dashboards from grafana to a git repository.",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
if err := backup(cmd.Context(), c); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
},
|
||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||
errs := c.Validate()
|
||||
for _, err := range errs {
|
||||
log.Error().Err(err).Send()
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
if err := cmd.Help(); err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
backupCmd.PersistentFlags().BoolVar(&c.ForceCommits, "force", false, "Force git commits / ignore existence check")
|
||||
backupCmd.PersistentFlags().StringVar(&c.GitEmail, "git-email", "", "Git email address")
|
||||
backupCmd.PersistentFlags().StringVar(&c.GPGKey, "signing-key", "", "Path to the GPG signing key")
|
||||
|
||||
return backupCmd
|
||||
}
|
||||
|
||||
func backup(ctx context.Context, cfg *config.Config) error {
|
||||
client := grafanabackuper.NewClient(
|
||||
grafanabackuper.WithZerologLogger(cfg.Logger),
|
||||
)
|
||||
|
||||
if err := client.GetSigner(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.Prepare(ctx, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Backup.Start(ctx, cfg)
|
||||
}
|
53
internal/cmd/restore.go
Normal file
53
internal/cmd/restore.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/config"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafanabackuper"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewRestoreCommand(c *config.Config) *cobra.Command {
|
||||
backupCmd := &cobra.Command{
|
||||
Use: "restore",
|
||||
Short: "Restore the dashboards from a git repository to grafana.",
|
||||
Long: "Restore the dashboards from a git repository to grafana.",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
if err := restore(cmd.Context(), c); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
},
|
||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||
errs := c.Validate()
|
||||
for _, err := range errs {
|
||||
log.Error().Err(err).Send()
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
if err := cmd.Help(); err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
backupCmd.PersistentFlags().BoolVar(&c.ForceCommits, "force", false, "Force dashboards / ignore existence check")
|
||||
|
||||
return backupCmd
|
||||
}
|
||||
|
||||
func restore(ctx context.Context, cfg *config.Config) error {
|
||||
client := grafanabackuper.NewClient(
|
||||
grafanabackuper.WithZerologLogger(cfg.Logger),
|
||||
)
|
||||
|
||||
if err := client.Prepare(ctx, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Restore.Start(ctx, cfg)
|
||||
}
|
84
internal/cmd/root.go
Normal file
84
internal/cmd/root.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/config"
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/logger"
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/version"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const envVarPrefix = "GB"
|
||||
|
||||
func NewRootCommand(c *config.Config) *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "grafana-backuper",
|
||||
Short: "Grafana Backuper CLI",
|
||||
Long: "A command-line tool to back up and restore Grafana dashboards",
|
||||
Version: version.Version(),
|
||||
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
|
||||
initializeConfig(cmd)
|
||||
|
||||
// c.Output = os.Stdout
|
||||
cmd.SetOut(os.Stdout)
|
||||
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
if c.Debug {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
} else if c.Quiet {
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
}
|
||||
|
||||
if c.JSONFormat {
|
||||
log.Logger = logger.JSONLoggerLayout(os.Stderr)
|
||||
c.Logger = logger.JSONLoggerLayout(os.Stdout)
|
||||
} else {
|
||||
log.Logger = logger.CliLoggerLayout(os.Stderr)
|
||||
c.Logger = logger.CliLoggerLayout(os.Stdout)
|
||||
}
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&c.Debug, "debug", "d", false, "Debug output")
|
||||
rootCmd.PersistentFlags().BoolVar(&c.JSONFormat, "json", false, "JSON output")
|
||||
rootCmd.PersistentFlags().BoolVar(&c.Quiet, "quiet", false, "Quiet output (only errors)")
|
||||
rootCmd.PersistentFlags().StringVar(&c.GrafanaURL, "grafana-url", "", "Grafana URL to access the API")
|
||||
rootCmd.PersistentFlags().StringVar(&c.GrafanaToken, "grafana-token", "", "Grafana auth token to access the API")
|
||||
rootCmd.PersistentFlags().StringVar(&c.GitBranch, "git-branch", "main", "Git branch name")
|
||||
rootCmd.PersistentFlags().StringVar(&c.GitRepo, "git-repo", "", "Complete Git repository URL")
|
||||
rootCmd.PersistentFlags().StringVar(&c.GitUser, "git-user", "", "Git user name")
|
||||
rootCmd.PersistentFlags().StringVar(&c.GitPass, "git-pass", "", "Git user password")
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
func initializeConfig(cmd *cobra.Command) {
|
||||
v := viper.New()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
v.SetEnvPrefix(envVarPrefix)
|
||||
v.AutomaticEnv()
|
||||
bindFlags(cmd, v)
|
||||
}
|
||||
|
||||
func bindFlags(cmd *cobra.Command, v *viper.Viper) {
|
||||
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
// Apply the viper config value to the flag when the flag is not set and viper has a value
|
||||
if !flag.Changed && v.IsSet(flag.Name) {
|
||||
err := cmd.Flags().Set(
|
||||
flag.Name,
|
||||
fmt.Sprintf("%v", v.Get(flag.Name)),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
56
internal/config/config.go
Normal file
56
internal/config/config.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Debug bool
|
||||
ForceCommits bool
|
||||
JSONFormat bool
|
||||
GrafanaURL string
|
||||
GrafanaToken string
|
||||
GitBranch string
|
||||
GitEmail string
|
||||
GitRepo string
|
||||
GitUser string
|
||||
GitPass string
|
||||
GPGKey string
|
||||
Quiet bool
|
||||
|
||||
Output io.Writer
|
||||
Logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (c *Config) Validate() []error {
|
||||
var errs []error
|
||||
|
||||
if c.GrafanaURL == "" {
|
||||
errs = append(errs, errors.New("invalid grafana URL, must not be blank"))
|
||||
}
|
||||
|
||||
if c.GrafanaToken == "" {
|
||||
errs = append(errs, errors.New("invalid auth token for grafana, must not be blank"))
|
||||
}
|
||||
|
||||
if c.GitRepo == "" {
|
||||
errs = append(errs, errors.New("invalid repo url for git, must not be blank"))
|
||||
}
|
||||
|
||||
if c.GitUser == "" {
|
||||
errs = append(errs, errors.New("invalid username for git, must not be blank"))
|
||||
}
|
||||
|
||||
if c.GitPass == "" {
|
||||
errs = append(errs, errors.New("invalid password for git, must not be blank"))
|
||||
}
|
||||
|
||||
if c.GitBranch == "" {
|
||||
errs = append(errs, errors.New("invalid branch name for git, must not be blank"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
28
internal/logger/logger.go
Normal file
28
internal/logger/logger.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func CliLoggerLayout(out io.Writer) zerolog.Logger {
|
||||
zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string {
|
||||
return fmt.Sprintf("%s:%d", filepath.Base(file), line)
|
||||
}
|
||||
|
||||
if zerolog.GlobalLevel() == zerolog.DebugLevel {
|
||||
debugOutput := zerolog.ConsoleWriter{Out: out, TimeFormat: time.RFC3339}
|
||||
return zerolog.New(debugOutput).With().Timestamp().Caller().Logger()
|
||||
}
|
||||
|
||||
output := zerolog.ConsoleWriter{Out: out, PartsExclude: []string{"time", "level"}}
|
||||
return zerolog.New(output).With().Logger()
|
||||
}
|
||||
|
||||
func JSONLoggerLayout(out io.Writer) zerolog.Logger {
|
||||
return zerolog.New(out).With().Timestamp().Caller().Logger()
|
||||
}
|
17
internal/version/version.go
Normal file
17
internal/version/version.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "undefined"
|
||||
versionPrerelease = "dev" //nolint:gochecknoglobals // this has to be a variable to set the version during release.
|
||||
)
|
||||
|
||||
func Version() string {
|
||||
if versionPrerelease != "" {
|
||||
return fmt.Sprintf("%s-%s", version, versionPrerelease)
|
||||
}
|
||||
return version
|
||||
}
|
85
main.go
85
main.go
|
@ -1,85 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/cfg"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/git"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana"
|
||||
)
|
||||
|
||||
func main() {
|
||||
appSettings := cfg.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := grafana.NewClient(appSettings.GrafanaURL, appSettings.GrafanaToken, grafana.DefaultHTTPClient)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating the grafana client: %v", err)
|
||||
}
|
||||
|
||||
gitdata := git.NewPayload(appSettings.GPGKey)
|
||||
if err = gitdata.GetRepo(appSettings.GitRepoURL, appSettings.GitUser, appSettings.GitPass); err != nil {
|
||||
log.Fatalf("Error cloning git repo: %v", err)
|
||||
}
|
||||
|
||||
gitdata.AddCommitter(appSettings.GitUser, appSettings.GitEmail)
|
||||
|
||||
dashboards, err := client.SearchDashboards(ctx, "", false)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching dashboards: %v", err)
|
||||
}
|
||||
|
||||
for _, dashboard := range dashboards {
|
||||
gitdata.UpdateDashboard(dashboard)
|
||||
|
||||
_, dashboardInfo, err := client.GetRawDashboardByUID(ctx, dashboard.UID)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching information of dashboard %s: %v", dashboard.Title, err)
|
||||
}
|
||||
|
||||
gitdata.UpdateDashboardInfo(dashboardInfo)
|
||||
|
||||
versions, err := client.GetDashboardVersionsByDashboardUID(ctx, dashboard.UID)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching versions for dashboard %s: %v", dashboard.Title, err)
|
||||
}
|
||||
|
||||
slices.Reverse(versions)
|
||||
|
||||
for _, version := range versions {
|
||||
gitdata.UpdateVersion(version)
|
||||
|
||||
if !appSettings.Force && gitdata.IsVersionCommitted(appSettings.GitBranch) {
|
||||
fmt.Printf("%s/%s - %s: %s -> already committed\n", dashboardInfo.FolderTitle, dashboard.Title, version.CreatedBy, version.Message)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("%s/%s - %s: %s\n", dashboardInfo.FolderTitle, dashboard.Title, version.CreatedBy, version.Message)
|
||||
|
||||
raw, info, err := client.GetRawDashboardByUIDAndVersion(ctx, dashboard.UID, version.Version)
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching dashboard %s version %d: %v", dashboard.Title, version.Version, err)
|
||||
}
|
||||
|
||||
gitdata.AddAuthor(info.CreatedBy, "")
|
||||
|
||||
output, err := grafana.ConvertRawToIndent(raw)
|
||||
if err != nil {
|
||||
log.Fatalf("Error pritty-printing dashboard %s version %d: %v", dashboard.Title, info.Version, err)
|
||||
}
|
||||
|
||||
gitdata.UpdateContent([]byte(output))
|
||||
gitdata.UpdateCommitter()
|
||||
if err = gitdata.CreateCommit(); err != nil {
|
||||
log.Fatalf("Error creating commit for dashboard %s version %d: %v", dashboard.Title, info.Version, err)
|
||||
}
|
||||
|
||||
if err = gitdata.PushToRemote(appSettings.GitUser, appSettings.GitPass); err != nil {
|
||||
log.Fatalf("Error pushing to remote repo %s: %v", appSettings.GitRepoURL, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
105
pkg/git/commit.go
Normal file
105
pkg/git/commit.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
type CommitOption func(*Commit)
|
||||
|
||||
func WithAuthor(name, email string) CommitOption {
|
||||
return func(c *Commit) {
|
||||
c.Author = &object.Signature{
|
||||
Name: name,
|
||||
Email: email,
|
||||
When: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithCommitter(name, email string) CommitOption {
|
||||
return func(c *Commit) {
|
||||
c.Committer = &object.Signature{
|
||||
Name: name,
|
||||
Email: email,
|
||||
When: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithFileContent(content []byte, filename, folder string) CommitOption {
|
||||
return func(c *Commit) {
|
||||
c.Content = content
|
||||
c.Filename = filepath.Join(folder, fmt.Sprintf("%s.json", filename))
|
||||
}
|
||||
}
|
||||
|
||||
func WithSigner(signKey SignKey) CommitOption {
|
||||
return func(c *Commit) {
|
||||
c.signKey = signKey.entity
|
||||
}
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Author *object.Signature
|
||||
Committer *object.Signature
|
||||
Content []byte
|
||||
Filename string
|
||||
KeyFile string
|
||||
|
||||
project *Project
|
||||
signKey *openpgp.Entity
|
||||
}
|
||||
|
||||
func (p *Project) NewCommit(options ...CommitOption) *Commit {
|
||||
commit := &Commit{project: p}
|
||||
|
||||
for _, option := range options {
|
||||
option(commit)
|
||||
}
|
||||
|
||||
return commit
|
||||
}
|
||||
|
||||
func (c *Commit) Create(msg string) error {
|
||||
if err := c.addContent(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.project.worktree.Add(c.Filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commitOpts := git.CommitOptions{Author: c.Author}
|
||||
|
||||
if c.Committer != nil {
|
||||
commitOpts.Committer = c.Committer
|
||||
}
|
||||
|
||||
if c.signKey != nil {
|
||||
commitOpts.SignKey = c.signKey
|
||||
}
|
||||
|
||||
_, err := c.project.worktree.Commit(msg, &commitOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commit) addContent() error {
|
||||
file, err := c.project.worktree.Filesystem.Create(c.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(c.Content)
|
||||
return err
|
||||
}
|
240
pkg/git/git.go
240
pkg/git/git.go
|
@ -1,240 +0,0 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
)
|
||||
|
||||
var (
|
||||
fs billy.Filesystem
|
||||
storer *memory.Storage
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
Author *object.Signature
|
||||
Committer *object.Signature
|
||||
Content []byte
|
||||
Dashboard grafana.FoundBoard
|
||||
DashboardInfo grafana.BoardProperties
|
||||
Directory string
|
||||
KeyFile string
|
||||
Repository *git.Repository
|
||||
Version grafana.DashboardVersion
|
||||
}
|
||||
|
||||
func NewPayload(keyFile string) *Payload {
|
||||
fs = memfs.New()
|
||||
storer = memory.NewStorage()
|
||||
|
||||
return &Payload{
|
||||
Author: nil,
|
||||
Committer: nil,
|
||||
Content: []byte{},
|
||||
Dashboard: grafana.FoundBoard{},
|
||||
DashboardInfo: grafana.BoardProperties{},
|
||||
Directory: "",
|
||||
KeyFile: keyFile,
|
||||
Repository: nil,
|
||||
Version: grafana.DashboardVersion{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) AddAuthor(name, email string) {
|
||||
p.Author = &object.Signature{
|
||||
Name: name,
|
||||
Email: email,
|
||||
When: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) UpdateAuthor(timestamp time.Time) {
|
||||
p.Author.When = timestamp
|
||||
}
|
||||
|
||||
func (p *Payload) AddCommitter(name, email string) {
|
||||
p.Committer = &object.Signature{
|
||||
Name: name,
|
||||
Email: email,
|
||||
When: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) UpdateCommitter() {
|
||||
p.Committer.When = time.Now()
|
||||
}
|
||||
|
||||
func (p *Payload) UpdateContent(content []byte) {
|
||||
p.Content = content
|
||||
}
|
||||
|
||||
func (p *Payload) UpdateDashboard(dashboard grafana.FoundBoard) {
|
||||
p.Dashboard = dashboard
|
||||
}
|
||||
|
||||
func (p *Payload) UpdateDashboardInfo(dashboardInfo grafana.BoardProperties) {
|
||||
p.DashboardInfo = dashboardInfo
|
||||
}
|
||||
|
||||
func (p *Payload) UpdateVersion(version grafana.DashboardVersion) {
|
||||
p.Version = version
|
||||
}
|
||||
|
||||
func (p *Payload) GetRepo(repoURL, user, password string) (err error) {
|
||||
p.Repository, err = git.Clone(
|
||||
storer,
|
||||
fs,
|
||||
&git.CloneOptions{
|
||||
Auth: genAuth(user, password),
|
||||
URL: repoURL,
|
||||
Progress: os.Stdout,
|
||||
},
|
||||
)
|
||||
|
||||
p.Directory = filepath.Base(repoURL)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Payload) IsVersionCommitted(branch string) bool {
|
||||
refName := plumbing.NewBranchReferenceName(branch)
|
||||
ref, err := p.Repository.Reference(refName, false)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
commitIter, err := p.Repository.Log(&git.LogOptions{From: ref.Hash()})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
err = commitIter.ForEach(func(commit *object.Commit) error {
|
||||
if strings.Contains(commit.Message, fmt.Sprintf("Update %s", p.Version.DashboardUID)) && strings.Contains(commit.Message, fmt.Sprintf("version %d", p.Version.ID)) {
|
||||
return fmt.Errorf("version already committed")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err != nil
|
||||
}
|
||||
|
||||
func genAuth(user, password string) *http.BasicAuth {
|
||||
return &http.BasicAuth{
|
||||
Username: user,
|
||||
Password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func commitDashboard(repo *git.Repository, content []byte, commitMsg, dashboardTitle, folderTitle, gitRepoDirectory, keyFile string, author, committer *object.Signature) (err error) {
|
||||
var (
|
||||
file billy.File
|
||||
signer *openpgp.Entity
|
||||
worktree *git.Worktree
|
||||
)
|
||||
|
||||
if strings.TrimSpace(keyFile) != "" {
|
||||
signer, err = getSigner(keyFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
worktree, err = repo.Worktree()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = fs.MkdirAll(folderTitle, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := filepath.Join(folderTitle, fmt.Sprintf("%s.json", dashboardTitle))
|
||||
if file, err = fs.Create(filePath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = file.Write(content); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = file.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = worktree.Add(filePath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = worktree.Commit(
|
||||
commitMsg,
|
||||
&git.CommitOptions{
|
||||
Author: author,
|
||||
Committer: committer,
|
||||
SignKey: signer,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Payload) CreateCommit() error {
|
||||
var commitmsg string
|
||||
|
||||
if p.Version.Message != "" {
|
||||
commitmsg = fmt.Sprintf(
|
||||
"%s: Update %s to version %d => %s",
|
||||
p.Dashboard.Title,
|
||||
p.Version.DashboardUID,
|
||||
p.Version.ID,
|
||||
p.Version.Message,
|
||||
)
|
||||
} else {
|
||||
commitmsg = fmt.Sprintf(
|
||||
"%s: Update %s to version %d",
|
||||
p.Dashboard.Title,
|
||||
p.Version.DashboardUID,
|
||||
p.Version.ID,
|
||||
)
|
||||
}
|
||||
|
||||
p.UpdateAuthor(p.Version.Created)
|
||||
p.UpdateCommitter()
|
||||
|
||||
return commitDashboard(
|
||||
p.Repository,
|
||||
p.Content,
|
||||
commitmsg,
|
||||
p.Dashboard.Title,
|
||||
p.DashboardInfo.FolderTitle,
|
||||
p.Directory,
|
||||
p.KeyFile,
|
||||
p.Author,
|
||||
p.Committer,
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Payload) PushToRemote(user, password string) error {
|
||||
origin, err := p.Repository.Remote("origin")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return origin.Push(
|
||||
&git.PushOptions{
|
||||
Auth: genAuth(user, password),
|
||||
Progress: os.Stdout,
|
||||
},
|
||||
)
|
||||
}
|
251
pkg/git/project.go
Normal file
251
pkg/git/project.go
Normal file
|
@ -0,0 +1,251 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
)
|
||||
|
||||
type ProjectOption func(*Project)
|
||||
|
||||
func WithBasicAuth(user, pass string) ProjectOption {
|
||||
return func(p *Project) {
|
||||
p.auth = &http.BasicAuth{
|
||||
Username: user,
|
||||
Password: pass,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithBranch(branch string) ProjectOption {
|
||||
return func(p *Project) {
|
||||
p.Branch = branch
|
||||
}
|
||||
}
|
||||
|
||||
func WithOutputWriter(o io.Writer) ProjectOption {
|
||||
return func(p *Project) {
|
||||
p.writer = o
|
||||
}
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Branch string
|
||||
Force bool
|
||||
RepoURL string
|
||||
CommitLogs map[string]*object.Commit
|
||||
|
||||
auth transport.AuthMethod
|
||||
fs billy.Filesystem
|
||||
storer *memory.Storage
|
||||
repository *git.Repository
|
||||
worktree *git.Worktree
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func NewProject(url string, options ...ProjectOption) *Project {
|
||||
project := &Project{
|
||||
RepoURL: url,
|
||||
CommitLogs: make(map[string]*object.Commit),
|
||||
fs: memfs.New(),
|
||||
storer: memory.NewStorage(),
|
||||
repository: nil,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(project)
|
||||
}
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
func (p *Project) Checkout() error {
|
||||
branchRef := plumbing.NewBranchReferenceName(p.Branch)
|
||||
|
||||
_, err := p.repository.Reference(branchRef, true)
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
var headRef *plumbing.Reference
|
||||
headRef, err = p.repository.Head()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := plumbing.NewHashReference(branchRef, headRef.Hash())
|
||||
if err = p.repository.Storer.SetReference(ref); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.worktree, err = p.repository.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkoutOpts := git.CheckoutOptions{
|
||||
Branch: branchRef,
|
||||
Create: false,
|
||||
}
|
||||
|
||||
if err = checkoutOpts.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.worktree.Checkout(&checkoutOpts)
|
||||
}
|
||||
|
||||
func (p *Project) Clone(ctx context.Context) error {
|
||||
cloneOpts := git.CloneOptions{
|
||||
URL: p.RepoURL,
|
||||
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
|
||||
}
|
||||
|
||||
if p.auth != nil {
|
||||
cloneOpts.Auth = p.auth
|
||||
}
|
||||
|
||||
if p.writer != nil {
|
||||
cloneOpts.Progress = p.writer
|
||||
}
|
||||
|
||||
if err := cloneOpts.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
p.repository, err = git.CloneContext(
|
||||
ctx,
|
||||
p.storer,
|
||||
p.fs,
|
||||
&cloneOpts,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Project) LoadLogs() error {
|
||||
commitIter, err := p.repository.Log(&git.LogOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return commitIter.ForEach(func(c *object.Commit) error {
|
||||
p.CommitLogs[c.Message] = c
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Project) CommitExists(commitmsg string) bool {
|
||||
if _, ok := p.CommitLogs[commitmsg]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Project) HasChanges() bool {
|
||||
localBranchRef, err := p.repository.Head()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
remoteBranchRef, err := p.repository.Reference(plumbing.NewRemoteReferenceName("origin", p.Branch), true)
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
return true
|
||||
} else if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return localBranchRef.Hash() != remoteBranchRef.Hash()
|
||||
}
|
||||
|
||||
func (p *Project) Pull(ctx context.Context) error {
|
||||
pullOpts := git.PullOptions{
|
||||
ReferenceName: plumbing.NewBranchReferenceName(p.Branch),
|
||||
}
|
||||
|
||||
if p.writer != nil {
|
||||
pullOpts.Progress = p.writer
|
||||
}
|
||||
|
||||
if err := pullOpts.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := p.worktree.PullContext(ctx, &pullOpts)
|
||||
if !errors.Is(err, plumbing.ErrReferenceNotFound) &&
|
||||
!errors.Is(err, git.NoErrAlreadyUpToDate) &&
|
||||
err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Project) Push(ctx context.Context) error {
|
||||
pushOpts := git.PushOptions{
|
||||
RemoteName: "origin",
|
||||
}
|
||||
|
||||
if p.auth != nil {
|
||||
pushOpts.Auth = p.auth
|
||||
}
|
||||
|
||||
if p.writer != nil {
|
||||
pushOpts.Progress = p.writer
|
||||
}
|
||||
|
||||
if err := pushOpts.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.repository.PushContext(ctx, &pushOpts)
|
||||
}
|
||||
|
||||
func (p *Project) ListJSONFiles(directory string) ([]string, error) {
|
||||
files, err := p.fs.ReadDir(directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var allFiles []string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
var childFiles []string
|
||||
childFiles, err = p.ListJSONFiles(filepath.Join(directory, file.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allFiles = append(allFiles, childFiles...)
|
||||
} else if strings.HasSuffix(file.Name(), ".json") {
|
||||
allFiles = append(allFiles, filepath.Join(directory, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
return allFiles, nil
|
||||
}
|
||||
|
||||
func (p *Project) ReadFile(filepath string) ([]byte, error) {
|
||||
file, err := p.fs.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return io.ReadAll(file)
|
||||
}
|
|
@ -7,22 +7,29 @@ import (
|
|||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
)
|
||||
|
||||
func getSigner(keyFile string) (*openpgp.Entity, error) {
|
||||
file, err := os.Open(keyFile)
|
||||
type SignKey struct {
|
||||
KeyFile string
|
||||
|
||||
entity *openpgp.Entity
|
||||
}
|
||||
|
||||
func (s *SignKey) ReadKeyFile() error {
|
||||
file, err := os.Open(s.KeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
block, err := armor.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
entityList, err := openpgp.ReadKeyRing(block.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err != nil || len(entityList) < 1 {
|
||||
return err
|
||||
}
|
||||
|
||||
return entityList[0], nil
|
||||
s.entity = entityList[0]
|
||||
return nil
|
||||
}
|
143
pkg/grafana/client.go
Normal file
143
pkg/grafana/client.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
package grafana
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
||||
const UserAgent = "grafana-backuper"
|
||||
|
||||
type Client struct {
|
||||
endpoint string
|
||||
token string
|
||||
tokenValid bool
|
||||
userAgent string
|
||||
httpClient *http.Client
|
||||
|
||||
Dashboard DashboardClient
|
||||
DashboardVersion DashboardVersionClient
|
||||
File SearchClient
|
||||
}
|
||||
|
||||
type ClientOption func(*Client)
|
||||
|
||||
func WithToken(token string) ClientOption {
|
||||
return func(client *Client) {
|
||||
client.token = token
|
||||
client.tokenValid = httpguts.ValidHeaderFieldName(token)
|
||||
}
|
||||
}
|
||||
|
||||
func WithHTTPClient(httpClient *http.Client) ClientOption {
|
||||
return func(client *Client) {
|
||||
client.httpClient = httpClient
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserAgent(userAgent string) ClientOption {
|
||||
return func(client *Client) {
|
||||
client.userAgent = userAgent
|
||||
}
|
||||
}
|
||||
|
||||
func NewClient(endpoint string, options ...ClientOption) *Client {
|
||||
client := &Client{
|
||||
endpoint: endpoint,
|
||||
tokenValid: true,
|
||||
httpClient: &http.Client{},
|
||||
userAgent: UserAgent,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(client)
|
||||
}
|
||||
|
||||
client.Dashboard = DashboardClient{client: client}
|
||||
client.DashboardVersion = DashboardVersionClient{client: client}
|
||||
client.File = SearchClient{client: client}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
|
||||
url := fmt.Sprintf("%s%s", c.endpoint, path)
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
|
||||
if !c.tokenValid {
|
||||
return nil, errors.New("authorization token contains invalid characters")
|
||||
}
|
||||
|
||||
if c.token != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
return req.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
*http.Response
|
||||
ErrorResponse *ErrorResponse
|
||||
|
||||
body []byte
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Message string
|
||||
TraceID uint
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, v any) (*Response, error) {
|
||||
httpResp, err := c.httpClient.Do(req)
|
||||
resp := &Response{Response: httpResp}
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
resp.body = body
|
||||
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errorResponse schema.ErrorResponse
|
||||
if err = json.Unmarshal(resp.body, &errorResponse); err == nil {
|
||||
return resp, fmt.Errorf(
|
||||
"grafana: got error with status code %d: %s",
|
||||
resp.StatusCode,
|
||||
ErrorResponseFromSchema(errorResponse).Message,
|
||||
)
|
||||
}
|
||||
return resp, fmt.Errorf("grafana: server responded with an unexpected status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
if writer, ok := v.(io.Writer); ok {
|
||||
_, err = io.Copy(writer, bytes.NewReader(resp.body))
|
||||
} else {
|
||||
err = json.Unmarshal(resp.body, v)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
|
@ -4,134 +4,147 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
|
||||
)
|
||||
|
||||
type BoardProperties struct {
|
||||
IsStarred bool `json:"isStarred,omitempty"`
|
||||
IsHome bool `json:"isHome,omitempty"`
|
||||
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanStar bool `json:"canStar"`
|
||||
Slug string `json:"slug"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Version int `json:"version"`
|
||||
FolderID int `json:"folderId"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderURL string `json:"folderUrl"`
|
||||
type Dashboard struct {
|
||||
IsStarred bool
|
||||
Type string
|
||||
CanSave bool
|
||||
CanEdit bool
|
||||
CanAdmin bool
|
||||
CanStar bool
|
||||
CanDelete bool
|
||||
Slug string
|
||||
URL string
|
||||
Expires time.Time
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
UpdatedBy string
|
||||
CreatedBy string
|
||||
Version uint
|
||||
HasACL bool
|
||||
IsFolder bool
|
||||
FolderID uint
|
||||
FolderUID string
|
||||
FolderTitle string
|
||||
FolderURL string
|
||||
Provisioned bool
|
||||
ProvisionedExternalID string
|
||||
AnnotationsPermissions AnnotationsPermissions
|
||||
Dashboard any
|
||||
}
|
||||
|
||||
type DashboardVersion struct {
|
||||
ID uint `json:"id"`
|
||||
DashboardID uint `json:"dashboardId"`
|
||||
DashboardUID string `json:"uid"`
|
||||
ParentVersion uint `json:"parentVersion"`
|
||||
RestoredFrom uint `json:"restoredFrom"`
|
||||
Version uint `json:"version"`
|
||||
Created time.Time `json:"created"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Message string `json:"message"`
|
||||
type AnnotationsPermissions struct {
|
||||
Dashboard AnnotationPermissions
|
||||
Organization AnnotationPermissions
|
||||
}
|
||||
|
||||
func (c *Client) getRawDashboardByUID(ctx context.Context, path string) ([]byte, BoardProperties, error) {
|
||||
raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil)
|
||||
type AnnotationPermissions struct {
|
||||
CanAdd bool
|
||||
CanEdit bool
|
||||
CanDelete bool
|
||||
}
|
||||
|
||||
type DashboardClient struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *DashboardClient) Get(ctx context.Context, uid string) (*Dashboard, *Response, error) {
|
||||
req, err := c.client.newRequest(ctx, "GET", fmt.Sprintf("/dashboards/uid/%s", uid), nil)
|
||||
if err != nil {
|
||||
return nil, BoardProperties{}, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if code != 200 {
|
||||
return raw, BoardProperties{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
|
||||
}
|
||||
var result struct {
|
||||
Meta BoardProperties `json:"meta"`
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&result); err != nil {
|
||||
return raw, BoardProperties{}, fmt.Errorf("failed unmarshalling dashboard from path %s: %v", path, err)
|
||||
}
|
||||
return raw, result.Meta, nil
|
||||
}
|
||||
|
||||
func (c *Client) getRawDashboardFromVersion(ctx context.Context, path string) ([]byte, DashboardVersion, error) {
|
||||
var versionInfo DashboardVersion
|
||||
raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil)
|
||||
var body schema.Dashboard
|
||||
|
||||
resp, err := c.client.do(req, &body)
|
||||
if err != nil {
|
||||
return nil, versionInfo, err
|
||||
return nil, resp, err
|
||||
}
|
||||
if code != 200 {
|
||||
return raw, versionInfo, fmt.Errorf("HTTP error %d: returns %s", code, raw)
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&versionInfo); err != nil {
|
||||
return raw, versionInfo, fmt.Errorf("failed unmarshalling dashboard from path %s: %v", path, err)
|
||||
}
|
||||
return raw, versionInfo, nil
|
||||
|
||||
return DashboardFromSchema(body), resp, nil
|
||||
}
|
||||
|
||||
func queryParams(params ...QueryParam) url.Values {
|
||||
u := url.URL{}
|
||||
q := u.Query()
|
||||
for _, p := range params {
|
||||
p(&q)
|
||||
}
|
||||
return q
|
||||
type DashboardCreateOpts struct {
|
||||
Dashboard any
|
||||
FolderID uint
|
||||
FolderUID string
|
||||
Message string
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
func (c *Client) GetDashboardVersionsByDashboardUID(ctx context.Context, uid string, params ...QueryParam) ([]DashboardVersion, error) {
|
||||
var (
|
||||
raw []byte
|
||||
code int
|
||||
err error
|
||||
)
|
||||
func (o DashboardCreateOpts) Validate() error {
|
||||
if o.Dashboard == nil {
|
||||
return errors.New("dashboard is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if raw, code, err = c.get(ctx, fmt.Sprintf("api/dashboards/uid/%s/versions", uid), queryParams(params...)); err != nil {
|
||||
type DashboardCreateResponse struct {
|
||||
DashboardID uint
|
||||
DashboardUID string
|
||||
URL string
|
||||
Status string
|
||||
Version uint
|
||||
Slug string
|
||||
}
|
||||
|
||||
func (c *DashboardClient) Create(
|
||||
ctx context.Context,
|
||||
opts DashboardCreateOpts,
|
||||
) (*DashboardCreateResponse, *Response, error) {
|
||||
if err := opts.Validate(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var reqBody schema.DashboardCreateRequest
|
||||
reqBody.Dashboard = opts.Dashboard
|
||||
reqBody.Overwrite = opts.Overwrite
|
||||
reqBody.Message = opts.Message
|
||||
if opts.FolderUID != "" {
|
||||
reqBody.FolderUID = opts.FolderUID
|
||||
} else if opts.FolderID > 0 {
|
||||
reqBody.FolderID = opts.FolderID
|
||||
}
|
||||
|
||||
reqBodyData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := c.client.newRequest(ctx, "POST", "/dashboards/db", bytes.NewReader(reqBodyData))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var respBody schema.DashboardCreateResponse
|
||||
resp, err := c.client.do(req, &respBody)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return DashboardCreateResponseFromSchema(respBody), resp, nil
|
||||
}
|
||||
|
||||
func (c *DashboardClient) Delete(ctx context.Context, uid string) (*Response, error) {
|
||||
req, err := c.client.newRequest(ctx, "DELETE", fmt.Sprintf("/dashboards/uid/%s", uid), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if code != 200 {
|
||||
return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
|
||||
}
|
||||
var versions []DashboardVersion
|
||||
err = json.Unmarshal(raw, &versions)
|
||||
|
||||
return versions, err
|
||||
return c.client.do(req, nil)
|
||||
}
|
||||
|
||||
func (c *Client) GetRawDashboardByUID(ctx context.Context, uid string) ([]byte, BoardProperties, error) {
|
||||
return c.getRawDashboardByUID(ctx, "uid/"+uid)
|
||||
}
|
||||
|
||||
func (c *Client) GetRawDashboardByUIDAndVersion(ctx context.Context, uid string, version uint) ([]byte, DashboardVersion, error) {
|
||||
return c.getRawDashboardFromVersion(ctx, "uid/"+uid+"/versions/"+fmt.Sprint(version))
|
||||
}
|
||||
|
||||
func (c *Client) SearchDashboards(ctx context.Context, query string, starred bool, tags ...string) ([]FoundBoard, error) {
|
||||
params := []SearchParam{
|
||||
SearchType(SearchTypeDashboard),
|
||||
SearchQuery(query),
|
||||
SearchStarred(starred),
|
||||
}
|
||||
for _, tag := range tags {
|
||||
params = append(params, SearchTag(tag))
|
||||
}
|
||||
return c.Search(ctx, params...)
|
||||
}
|
||||
|
||||
func ConvertRawToIndent(raw []byte) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := json.Indent(&buf, raw, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error pritty-printing raw json string: %v", err)
|
||||
func (c *DashboardClient) ParseRaw(raw []byte) (*Dashboard, error) {
|
||||
var body schema.Dashboard
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
return DashboardFromSchema(body), nil
|
||||
}
|
||||
|
|
183
pkg/grafana/dashboard_version.go
Normal file
183
pkg/grafana/dashboard_version.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package grafana
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
|
||||
)
|
||||
|
||||
type DashboardVersionParam func(*url.Values)
|
||||
|
||||
func WithLimit(limit uint) DashboardVersionParam {
|
||||
return func(v *url.Values) {
|
||||
v.Set("limit", strconv.FormatUint(uint64(limit), 10))
|
||||
}
|
||||
}
|
||||
|
||||
func WithStart(start uint) DashboardVersionParam {
|
||||
return func(v *url.Values) {
|
||||
v.Set("start", strconv.FormatUint(uint64(start), 10))
|
||||
}
|
||||
}
|
||||
|
||||
type DashboardVersion struct {
|
||||
ID uint
|
||||
DashboardID uint
|
||||
DashboardUID string
|
||||
ParentVersion uint
|
||||
RestoredFrom uint
|
||||
Version uint
|
||||
Created time.Time
|
||||
CreatedBy string
|
||||
Message string
|
||||
Data any
|
||||
}
|
||||
|
||||
type DashboardVersionClient struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *DashboardVersionClient) GetByID(
|
||||
ctx context.Context,
|
||||
id, version uint,
|
||||
params ...DashboardVersionParam,
|
||||
) (*DashboardVersion, *Response, error) {
|
||||
dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/id/%d/versions/%d", id, version))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c.get(ctx, dashboardVersionURL, params...)
|
||||
}
|
||||
|
||||
func (c *DashboardVersionClient) GetByUID(
|
||||
ctx context.Context,
|
||||
uid string,
|
||||
version uint,
|
||||
params ...DashboardVersionParam,
|
||||
) (*DashboardVersion, *Response, error) {
|
||||
dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/uid/%s/versions/%d", uid, version))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c.get(ctx, dashboardVersionURL, params...)
|
||||
}
|
||||
|
||||
func (c *DashboardVersionClient) Get(
|
||||
ctx context.Context,
|
||||
input string,
|
||||
version uint,
|
||||
params ...DashboardVersionParam,
|
||||
) (*DashboardVersion, *Response, error) {
|
||||
if id, err := strconv.Atoi(input); err == nil {
|
||||
return c.GetByID(ctx, uint(id), version, params...)
|
||||
}
|
||||
return c.GetByUID(ctx, input, version, params...)
|
||||
}
|
||||
|
||||
func (c *DashboardVersionClient) ListByID(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
params ...DashboardVersionParam,
|
||||
) ([]*DashboardVersion, *Response, error) {
|
||||
dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/id/%d/versions", id))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c.list(ctx, dashboardVersionURL, params...)
|
||||
}
|
||||
|
||||
func (c *DashboardVersionClient) ListByUID(
|
||||
ctx context.Context,
|
||||
uid string,
|
||||
params ...DashboardVersionParam,
|
||||
) ([]*DashboardVersion, *Response, error) {
|
||||
dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/uid/%s/versions", uid))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c.list(ctx, dashboardVersionURL, params...)
|
||||
}
|
||||
|
||||
func (c *DashboardVersionClient) List(
|
||||
ctx context.Context,
|
||||
input string,
|
||||
params ...DashboardVersionParam,
|
||||
) ([]*DashboardVersion, *Response, error) {
|
||||
if id, err := strconv.Atoi(input); err == nil {
|
||||
return c.ListByID(ctx, uint(id), params...)
|
||||
}
|
||||
return c.ListByUID(ctx, input, params...)
|
||||
}
|
||||
|
||||
func (c *DashboardVersionClient) get(
|
||||
ctx context.Context,
|
||||
dashboardVersionURL *url.URL,
|
||||
params ...DashboardVersionParam,
|
||||
) (*DashboardVersion, *Response, error) {
|
||||
if len(params) > 0 {
|
||||
query := dashboardVersionURL.Query()
|
||||
|
||||
for _, param := range params {
|
||||
param(&query)
|
||||
}
|
||||
|
||||
dashboardVersionURL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
req, err := c.client.newRequest(ctx, "GET", dashboardVersionURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var body schema.DashboardVersion
|
||||
|
||||
resp, err := c.client.do(req, &body)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return DashboardVersionFromSchema(body), resp, nil
|
||||
}
|
||||
|
||||
func (c *DashboardVersionClient) list(
|
||||
ctx context.Context,
|
||||
dashboardVersionURL *url.URL,
|
||||
params ...DashboardVersionParam,
|
||||
) ([]*DashboardVersion, *Response, error) {
|
||||
if len(params) > 0 {
|
||||
query := dashboardVersionURL.Query()
|
||||
|
||||
for _, param := range params {
|
||||
param(&query)
|
||||
}
|
||||
|
||||
dashboardVersionURL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
req, err := c.client.newRequest(ctx, "GET", dashboardVersionURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var body schema.DashboardVersionListResponse
|
||||
|
||||
resp, err := c.client.do(req, &body)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
dashboardVersions := make([]*DashboardVersion, 0, len(body))
|
||||
for _, dashboardVersion := range body {
|
||||
dashboardVersions = append(dashboardVersions, DashboardVersionFromSchema(dashboardVersion))
|
||||
}
|
||||
|
||||
return dashboardVersions, resp, nil
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
package grafana
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var DefaultHTTPClient = http.DefaultClient
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
key string
|
||||
basicAuth bool
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewClient(apiURL, authString string, client *http.Client) (*Client, error) {
|
||||
basicAuth := strings.Contains(authString, ":")
|
||||
baseURL, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var key string
|
||||
if len(authString) > 0 {
|
||||
if !basicAuth {
|
||||
key = fmt.Sprintf("Bearer %s", authString)
|
||||
} else {
|
||||
parts := strings.SplitN(authString, ":", 2)
|
||||
baseURL.User = url.UserPassword(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
return &Client{
|
||||
baseURL: baseURL.String(),
|
||||
basicAuth: basicAuth,
|
||||
key: key,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method, query string, params url.Values, buf io.Reader) ([]byte, int, error) {
|
||||
u, _ := url.Parse(c.baseURL)
|
||||
u.Path = path.Join(u.Path, query)
|
||||
if params != nil {
|
||||
u.RawQuery = params.Encode()
|
||||
}
|
||||
req, err := http.NewRequest(method, u.String(), buf)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if !c.basicAuth && len(c.key) > 0 {
|
||||
req.Header.Set("Authorization", c.key)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "grafana-backuper")
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return data, resp.StatusCode, err
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, query string, params url.Values) ([]byte, int, error) {
|
||||
return c.doRequest(ctx, "GET", query, params, nil)
|
||||
}
|
141
pkg/grafana/schema.go
Normal file
141
pkg/grafana/schema.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package grafana
|
||||
|
||||
import (
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
|
||||
)
|
||||
|
||||
func DashboardCreateResponseFromSchema(source schema.DashboardCreateResponse) *DashboardCreateResponse {
|
||||
return &DashboardCreateResponse{
|
||||
DashboardID: source.DashboardID,
|
||||
DashboardUID: source.DashboardUID,
|
||||
URL: source.URL,
|
||||
Status: source.Status,
|
||||
Version: source.Version,
|
||||
Slug: source.Slug,
|
||||
}
|
||||
}
|
||||
|
||||
func DashboardFromSchema(source schema.Dashboard) *Dashboard {
|
||||
return &Dashboard{
|
||||
IsStarred: source.Meta.IsStarred,
|
||||
Type: source.Meta.Type,
|
||||
CanSave: source.Meta.CanSave,
|
||||
CanEdit: source.Meta.CanEdit,
|
||||
CanAdmin: source.Meta.CanAdmin,
|
||||
CanStar: source.Meta.CanStar,
|
||||
CanDelete: source.Meta.CanDelete,
|
||||
Slug: source.Meta.Slug,
|
||||
URL: source.Meta.URL,
|
||||
Expires: source.Meta.Expires,
|
||||
Created: source.Meta.Created,
|
||||
Updated: source.Meta.Updated,
|
||||
UpdatedBy: source.Meta.UpdatedBy,
|
||||
CreatedBy: source.Meta.CreatedBy,
|
||||
Version: source.Meta.Version,
|
||||
HasACL: source.Meta.HasACL,
|
||||
IsFolder: source.Meta.IsFolder,
|
||||
FolderID: source.Meta.FolderID,
|
||||
FolderUID: source.Meta.FolderUID,
|
||||
FolderTitle: source.Meta.FolderTitle,
|
||||
FolderURL: source.Meta.FolderURL,
|
||||
Provisioned: source.Meta.Provisioned,
|
||||
ProvisionedExternalID: source.Meta.ProvisionedExternalID,
|
||||
AnnotationsPermissions: AnnotationsPermissions{
|
||||
Dashboard: AnnotationPermissions{
|
||||
CanAdd: source.Meta.AnnotationsPermissions.Dashboard.CanAdd,
|
||||
CanEdit: source.Meta.AnnotationsPermissions.Dashboard.CanEdit,
|
||||
CanDelete: source.Meta.AnnotationsPermissions.Dashboard.CanDelete,
|
||||
},
|
||||
Organization: AnnotationPermissions{
|
||||
CanAdd: source.Meta.AnnotationsPermissions.Organization.CanAdd,
|
||||
CanEdit: source.Meta.AnnotationsPermissions.Organization.CanEdit,
|
||||
CanDelete: source.Meta.AnnotationsPermissions.Organization.CanDelete,
|
||||
},
|
||||
},
|
||||
Dashboard: source.Dashboard,
|
||||
}
|
||||
}
|
||||
|
||||
func DashboardVersionFromSchema(source schema.DashboardVersion) *DashboardVersion {
|
||||
return &DashboardVersion{
|
||||
ID: source.ID,
|
||||
DashboardID: source.DashboardID,
|
||||
DashboardUID: source.UID,
|
||||
ParentVersion: source.ParentVersion,
|
||||
RestoredFrom: source.RestoredFrom,
|
||||
Version: source.Version,
|
||||
Created: source.Created,
|
||||
CreatedBy: source.CreatedBy,
|
||||
Message: source.Message,
|
||||
Data: source.Data,
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorResponseFromSchema(source schema.ErrorResponse) *ErrorResponse {
|
||||
return &ErrorResponse{
|
||||
Message: source.Message,
|
||||
TraceID: source.TraceID,
|
||||
}
|
||||
}
|
||||
|
||||
func SearchResultFromSchema(source schema.SearchResult) *SearchResult {
|
||||
return &SearchResult{
|
||||
ID: source.ID,
|
||||
UID: source.UID,
|
||||
Title: source.Title,
|
||||
URI: source.URI,
|
||||
URL: source.URL,
|
||||
Slug: source.Slug,
|
||||
Type: source.Type,
|
||||
Tags: source.Tags,
|
||||
IsStarred: source.IsStarred,
|
||||
SortMeta: source.SortMeta,
|
||||
FolderID: source.FolderID,
|
||||
FolderUID: source.FolderUID,
|
||||
FolderTitle: source.FolderTitle,
|
||||
FolderURL: source.FolderURL,
|
||||
}
|
||||
}
|
||||
|
||||
func SchemaFromDashboardMeta(dm *Dashboard) schema.Dashboard {
|
||||
return schema.Dashboard{
|
||||
Meta: schema.DashboardMeta{
|
||||
IsStarred: dm.IsStarred,
|
||||
Type: dm.Type,
|
||||
CanSave: dm.CanSave,
|
||||
CanEdit: dm.CanEdit,
|
||||
CanAdmin: dm.CanAdmin,
|
||||
CanStar: dm.CanStar,
|
||||
CanDelete: dm.CanDelete,
|
||||
Slug: dm.Slug,
|
||||
URL: dm.URL,
|
||||
Expires: dm.Expires,
|
||||
Created: dm.Created,
|
||||
Updated: dm.Updated,
|
||||
UpdatedBy: dm.UpdatedBy,
|
||||
CreatedBy: dm.CreatedBy,
|
||||
Version: dm.Version,
|
||||
HasACL: dm.HasACL,
|
||||
IsFolder: dm.IsFolder,
|
||||
FolderID: dm.FolderID,
|
||||
FolderUID: dm.FolderUID,
|
||||
FolderTitle: dm.FolderTitle,
|
||||
FolderURL: dm.FolderURL,
|
||||
Provisioned: dm.Provisioned,
|
||||
ProvisionedExternalID: dm.ProvisionedExternalID,
|
||||
AnnotationsPermissions: schema.AnnotationsPermissions{
|
||||
Dashboard: schema.AnnotationPermissions{
|
||||
CanAdd: dm.AnnotationsPermissions.Dashboard.CanAdd,
|
||||
CanEdit: dm.AnnotationsPermissions.Dashboard.CanEdit,
|
||||
CanDelete: dm.AnnotationsPermissions.Dashboard.CanDelete,
|
||||
},
|
||||
Organization: schema.AnnotationPermissions{
|
||||
CanAdd: dm.AnnotationsPermissions.Organization.CanAdd,
|
||||
CanEdit: dm.AnnotationsPermissions.Organization.CanEdit,
|
||||
CanDelete: dm.AnnotationsPermissions.Organization.CanDelete,
|
||||
},
|
||||
},
|
||||
},
|
||||
Dashboard: dm.Dashboard,
|
||||
}
|
||||
}
|
63
pkg/grafana/schema/dashboard.go
Normal file
63
pkg/grafana/schema/dashboard.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package schema
|
||||
|
||||
import "time"
|
||||
|
||||
type DashboardCreateRequest struct {
|
||||
Dashboard any `json:"dashboard"`
|
||||
FolderID uint `json:"folderId,omitempty"`
|
||||
FolderUID string `json:"folderUid"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
}
|
||||
|
||||
type DashboardCreateResponse struct {
|
||||
DashboardID uint `json:"id"`
|
||||
DashboardUID string `json:"uid"`
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"`
|
||||
Version uint `json:"version"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
type Dashboard struct {
|
||||
Meta DashboardMeta `json:"meta"`
|
||||
Dashboard any `json:"dashboard"`
|
||||
}
|
||||
|
||||
type DashboardMeta struct {
|
||||
IsStarred bool `json:"isStarred"`
|
||||
Type string `json:"type"`
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanAdmin bool `json:"canAdmin"`
|
||||
CanStar bool `json:"canStar"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
Slug string `json:"slug"`
|
||||
URL string `json:"url"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Version uint `json:"version"`
|
||||
HasACL bool `json:"hasAcl"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
FolderID uint `json:"folderId"`
|
||||
FolderUID string `json:"folderUid"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderURL string `json:"folderUrl"`
|
||||
Provisioned bool `json:"provisioned"`
|
||||
ProvisionedExternalID string `json:"provisionedExternalId"`
|
||||
AnnotationsPermissions AnnotationsPermissions `json:"annotationsPermissions"`
|
||||
}
|
||||
|
||||
type AnnotationsPermissions struct {
|
||||
Dashboard AnnotationPermissions `json:"dashboard"`
|
||||
Organization AnnotationPermissions `json:"organization"`
|
||||
}
|
||||
|
||||
type AnnotationPermissions struct {
|
||||
CanAdd bool `json:"canAdd"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
}
|
18
pkg/grafana/schema/dashboard_version.go
Normal file
18
pkg/grafana/schema/dashboard_version.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package schema
|
||||
|
||||
import "time"
|
||||
|
||||
type DashboardVersion struct {
|
||||
ID uint `json:"id"`
|
||||
DashboardID uint `json:"dashboardId"`
|
||||
UID string `json:"uid"`
|
||||
ParentVersion uint `json:"parentVersion"`
|
||||
RestoredFrom uint `json:"restoredFrom"`
|
||||
Version uint `json:"version"`
|
||||
Created time.Time `json:"created"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type DashboardVersionListResponse []DashboardVersion
|
6
pkg/grafana/schema/error.go
Normal file
6
pkg/grafana/schema/error.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package schema
|
||||
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
TraceID uint `json:"traceid"`
|
||||
}
|
20
pkg/grafana/schema/search.go
Normal file
20
pkg/grafana/schema/search.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package schema
|
||||
|
||||
type SearchResult struct {
|
||||
ID uint `json:"id"`
|
||||
UID string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
URI string `json:"uri"`
|
||||
URL string `json:"url"`
|
||||
Slug string `json:"slug"`
|
||||
Type string `json:"type"`
|
||||
Tags []string `json:"tags"`
|
||||
IsStarred bool `json:"isStarred"`
|
||||
SortMeta uint `json:"sortMeta"`
|
||||
FolderID int `json:"folderId"`
|
||||
FolderUID string `json:"folderUid"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderURL string `json:"folderUrl"`
|
||||
}
|
||||
|
||||
type SearchResultListResponse []SearchResult
|
|
@ -2,65 +2,30 @@ package grafana
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
|
||||
)
|
||||
|
||||
type (
|
||||
SearchParam func(*url.Values)
|
||||
|
||||
SearchType string
|
||||
)
|
||||
|
||||
const (
|
||||
SearchTypeFolder SearchParamType = "dash-folder"
|
||||
SearchTypeDashboard SearchParamType = "dash-db"
|
||||
SearchTypeFolder SearchType = "dash-folder"
|
||||
SearchTypeDashboard SearchType = "dash-db"
|
||||
)
|
||||
|
||||
type FoundBoard struct {
|
||||
ID uint `json:"id"`
|
||||
UID string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
URI string `json:"uri"`
|
||||
URL string `json:"url"`
|
||||
Slug string `json:"slug"`
|
||||
Type string `json:"type"`
|
||||
Tags []string `json:"tags"`
|
||||
IsStarred bool `json:"isStarred"`
|
||||
FolderID int `json:"folderId"`
|
||||
FolderUID string `json:"folderUid"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderURL string `json:"folderUrl"`
|
||||
func WithType(searchType SearchType) SearchParam {
|
||||
return func(v *url.Values) {
|
||||
v.Set("type", string(searchType))
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
// SearchParam is a type for specifying Search params.
|
||||
SearchParam func(*url.Values)
|
||||
// SearchParamType is a type accepted by SearchType func.
|
||||
SearchParamType string
|
||||
// QueryParam is a type for specifying arbitrary API parameters
|
||||
QueryParam func(*url.Values)
|
||||
)
|
||||
|
||||
func (c *Client) Search(ctx context.Context, params ...SearchParam) ([]FoundBoard, error) {
|
||||
var (
|
||||
raw []byte
|
||||
boards []FoundBoard
|
||||
code int
|
||||
err error
|
||||
)
|
||||
u := url.URL{}
|
||||
q := u.Query()
|
||||
for _, p := range params {
|
||||
p(&q)
|
||||
}
|
||||
if raw, code, err = c.get(ctx, "api/search", q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if code != 200 {
|
||||
return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
|
||||
}
|
||||
err = json.Unmarshal(raw, &boards)
|
||||
return boards, err
|
||||
}
|
||||
|
||||
func SearchQuery(query string) SearchParam {
|
||||
func WithQuery(query string) SearchParam {
|
||||
return func(v *url.Values) {
|
||||
if query != "" {
|
||||
v.Set("query", query)
|
||||
|
@ -68,13 +33,13 @@ func SearchQuery(query string) SearchParam {
|
|||
}
|
||||
}
|
||||
|
||||
func SearchStarred(starred bool) SearchParam {
|
||||
func WithStarred() SearchParam {
|
||||
return func(v *url.Values) {
|
||||
v.Set("starred", strconv.FormatBool(starred))
|
||||
v.Set("starred", strconv.FormatBool(true))
|
||||
}
|
||||
}
|
||||
|
||||
func SearchTag(tag string) SearchParam {
|
||||
func WithTag(tag string) SearchParam {
|
||||
return func(v *url.Values) {
|
||||
if tag != "" {
|
||||
v.Add("tag", tag)
|
||||
|
@ -82,8 +47,59 @@ func SearchTag(tag string) SearchParam {
|
|||
}
|
||||
}
|
||||
|
||||
func SearchType(searchType SearchParamType) SearchParam {
|
||||
return func(v *url.Values) {
|
||||
v.Set("type", string(searchType))
|
||||
}
|
||||
type SearchResult struct {
|
||||
ID uint
|
||||
UID string
|
||||
Title string
|
||||
URI string
|
||||
URL string
|
||||
Slug string
|
||||
Type string
|
||||
Tags []string
|
||||
IsStarred bool
|
||||
SortMeta uint
|
||||
FolderID int
|
||||
FolderUID string
|
||||
FolderTitle string
|
||||
FolderURL string
|
||||
}
|
||||
|
||||
type SearchClient struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *SearchClient) Search(ctx context.Context, params ...SearchParam) ([]*SearchResult, *Response, error) {
|
||||
searchURL, err := url.Parse("/search")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if len(params) > 0 {
|
||||
query := searchURL.Query()
|
||||
|
||||
for _, param := range params {
|
||||
param(&query)
|
||||
}
|
||||
|
||||
searchURL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
req, err := c.client.newRequest(ctx, "GET", searchURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var body schema.SearchResultListResponse
|
||||
|
||||
resp, err := c.client.do(req, &body)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
searchResults := make([]*SearchResult, 0, len(body))
|
||||
for _, searchResult := range body {
|
||||
searchResults = append(searchResults, SearchResultFromSchema(searchResult))
|
||||
}
|
||||
|
||||
return searchResults, resp, nil
|
||||
}
|
||||
|
|
159
pkg/grafanabackuper/backup.go
Normal file
159
pkg/grafanabackuper/backup.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package grafanabackuper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/config"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/git"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana"
|
||||
)
|
||||
|
||||
var ErrAlreadyCommited = errors.New("already committed")
|
||||
|
||||
type BackupClient struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (b *BackupClient) Start(ctx context.Context, cfg *config.Config) error {
|
||||
b.client.logger.Debug().Str("search-type", string(grafana.SearchTypeDashboard)).Msg("Searching dashboards by type")
|
||||
dashboards, _, err := b.client.grafana.File.Search(ctx, grafana.WithType(grafana.SearchTypeDashboard))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.client.logger.Debug().
|
||||
Str("search-type", string(grafana.SearchTypeDashboard)).
|
||||
Int("counter", len(dashboards)).
|
||||
Msg("Found dashboards")
|
||||
|
||||
for _, dashboardInfo := range dashboards {
|
||||
b.client.logger.Debug().
|
||||
Str("dashboard-uid", dashboardInfo.UID).
|
||||
Str("dashboard", dashboardInfo.Title).
|
||||
Msg("Fetching dashboard data")
|
||||
var dashboard *grafana.Dashboard
|
||||
dashboard, _, err = b.client.grafana.Dashboard.Get(ctx, dashboardInfo.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.client.logger.Debug().
|
||||
Str("dashboard-uid", dashboardInfo.UID).
|
||||
Str("dashboard", dashboardInfo.Title).
|
||||
Msg("Fetching dashboard versions")
|
||||
var versions []*grafana.DashboardVersion
|
||||
versions, _, err = b.client.grafana.DashboardVersion.List(ctx, dashboardInfo.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.client.logger.Debug().
|
||||
Str("dashboard-uid", dashboardInfo.UID).
|
||||
Str("dashboard", dashboardInfo.Title).
|
||||
Int("counter", len(versions)).
|
||||
Msg("Found dashboard versions")
|
||||
|
||||
slices.Reverse(versions)
|
||||
|
||||
uncommitedVersion := cfg.ForceCommits
|
||||
for _, version := range versions {
|
||||
b.client.logger.Debug().
|
||||
Str("dashboard-uid", dashboardInfo.UID).
|
||||
Uint("version", version.Version).
|
||||
Msg("Fetching version data")
|
||||
var dashboardVersion *grafana.DashboardVersion
|
||||
dashboardVersion, _, err = b.client.grafana.DashboardVersion.Get(ctx, dashboardInfo.UID, version.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var commitmsg string
|
||||
commitmsg, err = b.commitDashboardVersion(
|
||||
dashboard,
|
||||
dashboardVersion,
|
||||
dashboardInfo,
|
||||
cfg,
|
||||
uncommitedVersion,
|
||||
)
|
||||
if errors.Is(err, ErrAlreadyCommited) {
|
||||
b.client.logger.Info().Str("commit-msg", commitmsg).Msg("Already committed")
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uncommitedVersion = true
|
||||
b.client.logger.Info().Str("commit-msg", commitmsg).Msg("Commit created")
|
||||
}
|
||||
}
|
||||
|
||||
if !b.client.git.HasChanges() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.client.git.Push(ctx)
|
||||
}
|
||||
|
||||
func (b *BackupClient) commitDashboardVersion(
|
||||
dashboard *grafana.Dashboard,
|
||||
dashboardVersion *grafana.DashboardVersion,
|
||||
dashboardInfo *grafana.SearchResult,
|
||||
cfg *config.Config,
|
||||
force bool,
|
||||
) (string, error) {
|
||||
commitmsg := Message{
|
||||
ID: dashboardVersion.ID,
|
||||
Path: dashboardInfo.Title,
|
||||
Title: dashboardVersion.Message,
|
||||
UID: dashboardInfo.UID,
|
||||
}.String()
|
||||
|
||||
b.client.logger.Debug().Str("commit", commitmsg).Msg("Checking commit existence")
|
||||
if !force && b.client.git.CommitExists(commitmsg) {
|
||||
return commitmsg, ErrAlreadyCommited
|
||||
}
|
||||
|
||||
b.updateDashboardInfo(dashboard, dashboardVersion)
|
||||
|
||||
b.client.logger.Debug().
|
||||
Str("folder", dashboard.FolderTitle).
|
||||
Str("dashboard-uid", dashboardVersion.DashboardUID).
|
||||
Msg("Marshalling the dashboard version data")
|
||||
data, err := json.MarshalIndent(grafana.SchemaFromDashboardMeta(dashboard), "", " ")
|
||||
if err != nil {
|
||||
return commitmsg, err
|
||||
}
|
||||
|
||||
commitOpts := []git.CommitOption{
|
||||
git.WithAuthor(dashboardVersion.CreatedBy, ""),
|
||||
git.WithFileContent(data, dashboardInfo.Title, dashboard.FolderTitle),
|
||||
}
|
||||
|
||||
if b.client.signer != nil {
|
||||
commitOpts = append(commitOpts, git.WithSigner(*b.client.signer))
|
||||
}
|
||||
|
||||
if cfg.GitUser != "" {
|
||||
commitOpts = append(commitOpts, git.WithCommitter(cfg.GitUser, cfg.GitEmail))
|
||||
}
|
||||
|
||||
commit := b.client.git.NewCommit(commitOpts...)
|
||||
b.client.logger.Debug().Str("commit", commitmsg).Msg("Creating new commit")
|
||||
return commitmsg, commit.Create(commitmsg)
|
||||
}
|
||||
|
||||
func (b *BackupClient) updateDashboardInfo(d *grafana.Dashboard, dv *grafana.DashboardVersion) {
|
||||
b.client.logger.Debug().
|
||||
Str("dashboard-uid", dv.DashboardUID).
|
||||
Str("folder", d.FolderTitle).
|
||||
Msg("Updating dashboard information")
|
||||
d.Dashboard = dv.Data
|
||||
d.UpdatedBy = dv.CreatedBy
|
||||
d.Updated = dv.Created
|
||||
d.Version = dv.Version
|
||||
b.client.logger.Debug().
|
||||
Str("dashboard-uid", dv.DashboardUID).
|
||||
Str("folder", d.FolderTitle).
|
||||
Msg("Updated dashboard information successfully")
|
||||
}
|
86
pkg/grafanabackuper/client.go
Normal file
86
pkg/grafanabackuper/client.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package grafanabackuper
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/config"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/git"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type ClientOption func(*Client)
|
||||
|
||||
func WithZerologLogger(logger zerolog.Logger) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
git *git.Project
|
||||
grafana *grafana.Client
|
||||
logger zerolog.Logger
|
||||
signer *git.SignKey
|
||||
|
||||
Backup BackupClient
|
||||
Restore RestoreClient
|
||||
}
|
||||
|
||||
func NewClient(options ...ClientOption) *Client {
|
||||
client := &Client{}
|
||||
|
||||
for _, option := range options {
|
||||
option(client)
|
||||
}
|
||||
|
||||
client.Backup = BackupClient{client: client}
|
||||
client.Restore = RestoreClient{client: client}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) Prepare(ctx context.Context, cfg *config.Config) error {
|
||||
c.logger.Debug().Msg("Creating new Grafana Client")
|
||||
c.grafana = grafana.NewClient(
|
||||
cfg.GrafanaURL,
|
||||
grafana.WithToken(cfg.GrafanaToken),
|
||||
)
|
||||
c.logger.Debug().Msg("Created new Grafana Client successfully")
|
||||
|
||||
c.git = git.NewProject(
|
||||
cfg.GitRepo,
|
||||
git.WithBasicAuth(cfg.GitUser, cfg.GitPass),
|
||||
git.WithBranch(cfg.GitBranch),
|
||||
git.WithOutputWriter(cfg.Output),
|
||||
)
|
||||
|
||||
c.logger.Debug().Msg("Cloning git project")
|
||||
if err := c.git.Clone(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.logger.Debug().Msg("Checking out git project")
|
||||
if err := c.git.Checkout(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.logger.Debug().Msg("Pulling git project content")
|
||||
if err := c.git.Pull(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.logger.Debug().Msg("Loading git logs")
|
||||
return c.git.LoadLogs()
|
||||
}
|
||||
|
||||
func (c *Client) GetSigner(cfg *config.Config) error {
|
||||
if cfg.GPGKey == "" {
|
||||
c.logger.Debug().Msg("GPG key path parameter is empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
c.signer = &git.SignKey{KeyFile: cfg.GPGKey}
|
||||
c.logger.Debug().Msg("Reading GPG key file")
|
||||
return c.signer.ReadKeyFile()
|
||||
}
|
22
pkg/grafanabackuper/message.go
Normal file
22
pkg/grafanabackuper/message.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package grafanabackuper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
ID uint
|
||||
Path string
|
||||
Title string
|
||||
UID string
|
||||
}
|
||||
|
||||
func (m Message) String() string {
|
||||
commitMsg := fmt.Sprintf("%s: Update %s to version %d", m.Path, m.UID, m.ID)
|
||||
|
||||
if m.Title != "" {
|
||||
commitMsg += fmt.Sprintf(" => %s", m.Title)
|
||||
}
|
||||
|
||||
return commitMsg
|
||||
}
|
103
pkg/grafanabackuper/restore.go
Normal file
103
pkg/grafanabackuper/restore.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package grafanabackuper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.ar21.de/yolokube/grafana-backuper/internal/config"
|
||||
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana"
|
||||
)
|
||||
|
||||
type RestoreClient struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (r *RestoreClient) Start(ctx context.Context, cfg *config.Config) error {
|
||||
files, err := r.client.git.ListJSONFiles("./")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.client.logger.Debug().Int("files", len(files)).Msg("Collected all files")
|
||||
|
||||
for _, filename := range files {
|
||||
r.client.logger.Debug().Str("file", filename).Msg("Reading data")
|
||||
var data []byte
|
||||
data, err = r.client.git.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.client.logger.Debug().Msg("Parsing raw dashboard data")
|
||||
var dashboard *grafana.Dashboard
|
||||
dashboard, err = r.client.grafana.Dashboard.ParseRaw(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, ok := dashboard.Dashboard.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
uid := fmt.Sprint(content["uid"])
|
||||
title := fmt.Sprint(content["title"])
|
||||
|
||||
r.client.logger.Debug().
|
||||
Str("dashboard-uid", uid).
|
||||
Str("title", title).
|
||||
Msg("Fetching current dashboard version from Grafana")
|
||||
var current *grafana.Dashboard
|
||||
current, _, err = r.client.grafana.Dashboard.Get(ctx, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if current.Version == dashboard.Version && !cfg.ForceCommits {
|
||||
r.client.logger.Info().
|
||||
Any("dashboard-uid", uid).
|
||||
Str("folder", dashboard.FolderTitle).
|
||||
Str("dashboard", title).
|
||||
Msg("Dashboard already up to date")
|
||||
continue
|
||||
}
|
||||
|
||||
r.client.logger.Debug().Str("dashboard-uid", uid).Str("title", title).Msg("Syncing dashboard with Grafana")
|
||||
var createResp *grafana.DashboardCreateResponse
|
||||
createResp, err = r.syncDashboard(ctx, dashboard, cfg.ForceCommits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.client.logger.Info().Any("resp", createResp).Msg("Created / Updated dashboard successfully")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RestoreClient) syncDashboard(
|
||||
ctx context.Context,
|
||||
d *grafana.Dashboard,
|
||||
force bool,
|
||||
) (*grafana.DashboardCreateResponse, error) {
|
||||
createOpts := grafana.DashboardCreateOpts{
|
||||
Dashboard: d.Dashboard,
|
||||
FolderUID: d.FolderUID,
|
||||
Message: "sync git repository to grafana",
|
||||
Overwrite: force,
|
||||
}
|
||||
|
||||
r.client.logger.Debug().Msg("Validating create options")
|
||||
if err := createOpts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createResp, resp, err := r.client.grafana.Dashboard.Create(ctx, createOpts)
|
||||
if err != nil {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
r.client.logger.Debug().Str("resp", string(body)).Msg("Got error during dashboard creation / update")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createResp, nil
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
"config:base",
|
||||
"docker:enableMajor"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
|
@ -11,5 +12,10 @@
|
|||
"platformAutomerge": true,
|
||||
"dependencyDashboard": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"postUpdateOptions": [
|
||||
"gomodTidy",
|
||||
"gomodUpdateImportPaths"
|
||||
],
|
||||
"semanticCommits": "enabled"
|
||||
}
|
Loading…
Reference in a new issue