Merge pull request 'Refactor entire project' (#16) from make_linters_happy into main
Reviewed-on: #16
This commit is contained in:
commit
a8c862cc37
45 changed files with 2816 additions and 855 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
|
|
||||||
|
|
358
.golangci.yml
Normal file
358
.golangci.yml
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
# This code is licensed under the terms of the MIT license https://opensource.org/license/mit
|
||||||
|
# Copyright (c) 2024 Marat Reymers
|
||||||
|
|
||||||
|
## Golden config for golangci-lint v1.62.0
|
||||||
|
#
|
||||||
|
# This is the best config for golangci-lint based on my experience and opinion.
|
||||||
|
# It is very strict, but not extremely strict.
|
||||||
|
# Feel free to adapt and change it for your needs.
|
||||||
|
|
||||||
|
run:
|
||||||
|
# Timeout for analysis, e.g. 30s, 5m.
|
||||||
|
# Default: 1m
|
||||||
|
timeout: 3m
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
gochecksumtype:
|
||||||
|
# Presence of `default` case in switch statements satisfies exhaustiveness, if all members are not listed.
|
||||||
|
# Default: true
|
||||||
|
default-signifies-exhaustive: false
|
||||||
|
|
||||||
|
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 (Go 1.22+)
|
||||||
|
- 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
|
||||||
|
- 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
|
||||||
|
- iface # checks the incorrect use of interfaces, helping developers avoid interface pollution
|
||||||
|
- 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
|
||||||
|
- recvcheck # checks for receiver type consistency
|
||||||
|
- 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
|
||||||
|
#- exportloopref # [not necessary from Go 1.22] checks for pointers to enclosing loop variables
|
||||||
|
#- 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
|
||||||
|
- errcheck
|
||||||
|
- funlen
|
||||||
|
- goconst
|
||||||
|
- gosec
|
||||||
|
- noctx
|
||||||
|
- wrapcheck
|
42
.woodpecker/.build.yaml
Normal file
42
.woodpecker/.build.yaml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
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
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
tags:
|
||||||
|
- staging
|
||||||
|
- staging-${CI_PIPELINE_NUMBER}
|
||||||
|
dry_run: true
|
||||||
|
when:
|
||||||
|
- branch:
|
||||||
|
exclude: main
|
||||||
|
event: push
|
||||||
|
depends_on:
|
||||||
|
- lint
|
||||||
|
- test
|
59
.woodpecker/.deploy.yaml
Normal file
59
.woodpecker/.deploy.yaml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
skip_clone: true
|
||||||
|
steps:
|
||||||
|
- name: bump tag in deployment-repo
|
||||||
|
image: git.ar21.de/aaron/kustomize-ci
|
||||||
|
commands:
|
||||||
|
- git clone https://git.ar21.de/yolokube/core-deployments.git deployment-repo
|
||||||
|
- cd deployment-repo/grafana-backuper
|
||||||
|
- 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: "${CI_PIPELINE_NUMBER}_grafana_backuper"
|
||||||
|
remote: ssh://git@git.ar21.de:2222/yolokube/core-deployments.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
|
||||||
|
- name: create pull request
|
||||||
|
image: git.ar21.de/tom/push-message-randomizer:latest
|
||||||
|
pull: true
|
||||||
|
settings:
|
||||||
|
gitea_address: https://git.ar21.de
|
||||||
|
gitea_token:
|
||||||
|
from_secret: FORGEJO_API
|
||||||
|
owner: ${CI_REPO_OWNER}
|
||||||
|
repo: core-deployments
|
||||||
|
branch: "${CI_PIPELINE_NUMBER}_grafana_backuper"
|
||||||
|
base_branch: main
|
||||||
|
pr_title: "GRAFANA BACKUPER: update image tag to ${CI_PIPELINE_NUMBER}"
|
||||||
|
pr_body: |
|
||||||
|
### ℹ Grafana-Backuper image update
|
||||||
|
|
||||||
|
{- random-pr-message -}
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
📅 **Schedule**: Branch creation - At any time, Automerge - At any time.
|
||||||
|
|
||||||
|
🚦 **Automerge**: Enabled.
|
||||||
|
skip_on_missing_branch: true
|
||||||
|
close_pr_if_empty: true
|
||||||
|
delete_branch_if_pr_empty: true
|
||||||
|
merge_when_checks_succeed: true
|
||||||
|
delete_branch_after_merge: true
|
||||||
|
when:
|
||||||
|
- branch: main
|
||||||
|
event: push
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
- lint
|
||||||
|
- test
|
20
.woodpecker/.lint.yaml
Normal file
20
.woodpecker/.lint.yaml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
steps:
|
||||||
|
- name: gofmt
|
||||||
|
image: golang:1.22.5
|
||||||
|
commands:
|
||||||
|
- gofmt -l -s .
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
- name: vuln-check
|
||||||
|
image: golang:1.22.5
|
||||||
|
commands:
|
||||||
|
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
- govulncheck ./...
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
- name: golangci-linter
|
||||||
|
image: golangci/golangci-lint:v1.62.2
|
||||||
|
commands:
|
||||||
|
- golangci-lint run ./...
|
||||||
|
when:
|
||||||
|
- event: push
|
9
.woodpecker/.test.yaml
Normal file
9
.woodpecker/.test.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
steps:
|
||||||
|
- name: gotest
|
||||||
|
image: golang:1.22.5
|
||||||
|
commands:
|
||||||
|
- go test ./...
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
depends_on:
|
||||||
|
- lint
|
16
Dockerfile
16
Dockerfile
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:1.22.4-bookworm AS build
|
FROM golang:1.22.5-bookworm AS build
|
||||||
|
|
||||||
# Create build workspace folder
|
# Create build workspace folder
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
@ -9,7 +9,7 @@ RUN apt-get update --yes && \
|
||||||
apt-get install --yes build-essential
|
apt-get install --yes build-essential
|
||||||
|
|
||||||
# Build the actual binary
|
# 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
|
||||||
|
|
||||||
# -- -- -- -- -- --
|
# -- -- -- -- -- --
|
||||||
|
|
||||||
|
@ -20,6 +20,14 @@ FROM alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy built binary from build image
|
# Copy built binary from build image
|
||||||
COPY --from=build /workspace/grafana-backuper /app
|
COPY --from=build /workspace/grafana-backuper .
|
||||||
|
|
||||||
ENTRYPOINT ["/app/grafana-backuper"]
|
RUN chmod +x grafana-backuper
|
||||||
|
|
||||||
|
# Copy the wrapper script
|
||||||
|
COPY entrypoint.sh .
|
||||||
|
|
||||||
|
# Ensure the script is executable
|
||||||
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
|
|
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)
|
||||||
|
}
|
||||||
|
}
|
8
entrypoint.sh
Normal file
8
entrypoint.sh
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if the environment variable GRAFANA_MODE is set
|
||||||
|
if [ -z "$GB_SEQUENCE" ]; then
|
||||||
|
exec ./grafana-backuper backup --json
|
||||||
|
else
|
||||||
|
exec ./grafana-backuper --json
|
||||||
|
fi
|
47
go.mod
47
go.mod
|
@ -1,32 +1,55 @@
|
||||||
module git.ar21.de/yolokube/grafana-backuper
|
module git.ar21.de/yolokube/grafana-backuper
|
||||||
|
|
||||||
go 1.22.2
|
go 1.22.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/go-crypto v1.0.0
|
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-billy/v5 v5.5.0
|
||||||
github.com/go-git/go-git/v5 v5.12.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
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
golang.org/x/net v0.28.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/cloudflare/circl v1.3.7 // indirect
|
github.com/cloudflare/circl v1.3.9 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
github.com/cyphar/filepath-securejoin v0.3.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/emirpasic/gods v1.18.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/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // 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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // 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/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/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
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.21.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.15.0 // indirect
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
golang.org/x/net v0.22.0 // indirect
|
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||||
golang.org/x/sys v0.18.0 // indirect
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
golang.org/x/tools v0.18.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/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.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
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.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
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 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
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 h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
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 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
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/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.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
|
||||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
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.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.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 h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
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 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
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 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-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 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||||
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
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 h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
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 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
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 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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.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 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
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 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
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/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.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
|
||||||
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
|
||||||
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
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.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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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.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 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
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=
|
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-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-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.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.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.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
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.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.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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
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.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.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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.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.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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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-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-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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.18.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-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.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.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.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.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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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-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.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.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.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=
|
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 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
58
internal/cmd/backup.go
Normal file
58
internal/cmd/backup.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:dupl // This function may be the same as or similar to other cmd functions.
|
||||||
|
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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
54
internal/cmd/restore.go
Normal file
54
internal/cmd/restore.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:dupl // This function may be the same as or similar to other cmd functions.
|
||||||
|
func NewRestoreCommand(c *config.Config) *cobra.Command {
|
||||||
|
restoreCmd := &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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreCmd.PersistentFlags().BoolVar(&c.ForceCommits, "force", false, "Force dashboards / ignore existence check")
|
||||||
|
|
||||||
|
return restoreCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
102
internal/cmd/root.go
Normal file
102
internal/cmd/root.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
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(),
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if len(c.Sequence) == 0 {
|
||||||
|
return cmd.Help()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, command := range c.Sequence {
|
||||||
|
log.Info().Str("function", command).Msg("Executing function")
|
||||||
|
cmd.SetArgs([]string{command})
|
||||||
|
if err := cmd.Execute(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
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")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&c.GitEmail, "git-email", "", "Git email address")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&c.GPGKey, "signing-key", "", "Path to the GPG signing key")
|
||||||
|
rootCmd.Flags().StringSliceVar(&c.Sequence, "sequence", nil, "Command sequence to execute multiple functions")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
66
internal/config/config.go
Normal file
66
internal/config/config.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidGrafanaURL = errors.New("invalid grafana URL, must not be blank")
|
||||||
|
ErrInvalidAuthToken = errors.New("invalid auth token for grafana, must not be blank")
|
||||||
|
ErrInvalidRepoURL = errors.New("invalid repo url for git, must not be blank")
|
||||||
|
ErrInvalidGitUser = errors.New("invalid username for git, must not be blank")
|
||||||
|
ErrInvalidGitPass = errors.New("invalid password for git, must not be blank")
|
||||||
|
ErrInvalidBranchName = errors.New("invalid branch name for git, must not be blank")
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
Sequence []string
|
||||||
|
|
||||||
|
Output io.Writer
|
||||||
|
Logger zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Validate() []error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if c.GrafanaURL == "" {
|
||||||
|
errs = append(errs, ErrInvalidGrafanaURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.GrafanaToken == "" {
|
||||||
|
errs = append(errs, ErrInvalidAuthToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.GitRepo == "" {
|
||||||
|
errs = append(errs, ErrInvalidRepoURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.GitUser == "" {
|
||||||
|
errs = append(errs, ErrInvalidGitUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.GitPass == "" {
|
||||||
|
errs = append(errs, ErrInvalidGitPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.GitBranch == "" {
|
||||||
|
errs = append(errs, ErrInvalidBranchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
40
internal/config/config_test.go
Normal file
40
internal/config/config_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ar21.de/yolokube/grafana-backuper/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Validate_MissingFields(t *testing.T) {
|
||||||
|
cfg := config.Config{}
|
||||||
|
|
||||||
|
errs := cfg.Validate()
|
||||||
|
assert.Len(t, errs, 6) // Expecting 6 errors since all required fields are missing
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_AllFieldsPresent(t *testing.T) {
|
||||||
|
cfg := config.Config{
|
||||||
|
GrafanaURL: "http://grafana.example.com",
|
||||||
|
GrafanaToken: "sometoken",
|
||||||
|
GitRepo: "https://github.com/user/repo",
|
||||||
|
GitUser: "username",
|
||||||
|
GitPass: "password",
|
||||||
|
GitBranch: "main",
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := cfg.Validate()
|
||||||
|
assert.Empty(t, errs) // No errors should be returned when all fields are valid
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_PartiallyPopulated(t *testing.T) {
|
||||||
|
cfg := config.Config{
|
||||||
|
GrafanaURL: "http://grafana.example.com",
|
||||||
|
GitRepo: "https://github.com/user/repo",
|
||||||
|
GitUser: "username",
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := cfg.Validate()
|
||||||
|
assert.Len(t, errs, 3) // Expecting 3 errors for missing GrafanaToken, GitPass, and GitBranch
|
||||||
|
}
|
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()
|
||||||
|
}
|
49
internal/logger/logger_test.go
Normal file
49
internal/logger/logger_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package logger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ar21.de/yolokube/grafana-backuper/internal/logger"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCliLoggerLayout_DebugLevel(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
|
||||||
|
log := logger.CliLoggerLayout(&buf)
|
||||||
|
log.Debug().Msg("test message")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
assert.Contains(t, output, "test message")
|
||||||
|
assert.Contains(t, output, "DBG") // Assuming zerolog adds a short level indicator like "DBG"
|
||||||
|
assert.Contains(t, output, ".go:") // Ensures the caller file and line are included
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCliLoggerLayout_NonDebugLevel(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
|
||||||
|
log := logger.CliLoggerLayout(&buf)
|
||||||
|
log.Info().Msg("test message")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
assert.Contains(t, output, "test message")
|
||||||
|
assert.NotContains(t, output, "DBG") // Since we're at Info level
|
||||||
|
assert.NotContains(t, output, ".go:") // Caller information should not be included
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONLoggerLayout(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
log := logger.JSONLoggerLayout(&buf)
|
||||||
|
log.Info().Msg("test message")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
assert.Contains(t, output, `"message":"test message"`)
|
||||||
|
assert.Contains(t, output, `"level":"info"`)
|
||||||
|
assert.Contains(t, output, `"time":`) // Timestamp should be included
|
||||||
|
assert.Contains(t, output, `"caller":`) // Caller should be included
|
||||||
|
}
|
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)
|
||||||
|
}
|
14
pkg/git/project_test.go
Normal file
14
pkg/git/project_test.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package git_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ar21.de/yolokube/grafana-backuper/pkg/git"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewProject(t *testing.T) {
|
||||||
|
project := git.NewProject("https://example.com/repo.git")
|
||||||
|
assert.NotNil(t, project)
|
||||||
|
assert.Equal(t, "https://example.com/repo.git", project.RepoURL)
|
||||||
|
}
|
|
@ -7,22 +7,29 @@ import (
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getSigner(keyFile string) (*openpgp.Entity, error) {
|
type SignKey struct {
|
||||||
file, err := os.Open(keyFile)
|
KeyFile string
|
||||||
|
|
||||||
|
entity *openpgp.Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignKey) ReadKeyFile() error {
|
||||||
|
file, err := os.Open(s.KeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
block, err := armor.Decode(file)
|
block, err := armor.Decode(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
entityList, err := openpgp.ReadKeyRing(block.Body)
|
entityList, err := openpgp.ReadKeyRing(block.Body)
|
||||||
if err != nil {
|
if err != nil || len(entityList) < 1 {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return entityList[0], nil
|
s.entity = entityList[0]
|
||||||
|
return nil
|
||||||
}
|
}
|
36
pkg/git/sign_key_test.go
Normal file
36
pkg/git/sign_key_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package git_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ar21.de/yolokube/grafana-backuper/pkg/git"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadKeyFile_Success(t *testing.T) {
|
||||||
|
signKey := &git.SignKey{KeyFile: "testdata/test-key.asc"}
|
||||||
|
err := signKey.ReadKeyFile()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadKeyFile_FileError(t *testing.T) {
|
||||||
|
signKey := &git.SignKey{KeyFile: "nonexistent-file.asc"}
|
||||||
|
err := signKey.ReadKeyFile()
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadKeyFile_DecodingError(t *testing.T) {
|
||||||
|
signKey := &git.SignKey{KeyFile: "testdata/test-invalid-key.asc"}
|
||||||
|
err := signKey.ReadKeyFile()
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadKeyFile_EmptyKeyRing(t *testing.T) {
|
||||||
|
signKey := &git.SignKey{KeyFile: "testdata/test-empty-key.asc"}
|
||||||
|
err := signKey.ReadKeyFile()
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
4
pkg/git/testdata/test-empty-key.asc
vendored
Normal file
4
pkg/git/testdata/test-empty-key.asc
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
Version: Keybase OpenPGP v1.0.0
|
||||||
|
Comment: https://keybase.io/crypto
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
1
pkg/git/testdata/test-invalid-key.asc
vendored
Normal file
1
pkg/git/testdata/test-invalid-key.asc
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
invalid content
|
182
pkg/git/testdata/test-key.asc
vendored
Normal file
182
pkg/git/testdata/test-key.asc
vendored
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
Version: Keybase OpenPGP v1.0.0
|
||||||
|
Comment: https://keybase.io/crypto
|
||||||
|
|
||||||
|
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
|
||||||
|
YZ3g9Y8QOzszYPB450rVxLOEY6eV1Q==
|
||||||
|
=W3yz
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
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"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BoardProperties struct {
|
type Dashboard struct {
|
||||||
IsStarred bool `json:"isStarred,omitempty"`
|
IsStarred bool
|
||||||
IsHome bool `json:"isHome,omitempty"`
|
Type string
|
||||||
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
CanSave bool
|
||||||
Type string `json:"type,omitempty"`
|
CanEdit bool
|
||||||
CanSave bool `json:"canSave"`
|
CanAdmin bool
|
||||||
CanEdit bool `json:"canEdit"`
|
CanStar bool
|
||||||
CanStar bool `json:"canStar"`
|
CanDelete bool
|
||||||
Slug string `json:"slug"`
|
Slug string
|
||||||
Expires time.Time `json:"expires"`
|
URL string
|
||||||
Created time.Time `json:"created"`
|
Expires time.Time
|
||||||
Updated time.Time `json:"updated"`
|
Created time.Time
|
||||||
UpdatedBy string `json:"updatedBy"`
|
Updated time.Time
|
||||||
CreatedBy string `json:"createdBy"`
|
UpdatedBy string
|
||||||
Version int `json:"version"`
|
CreatedBy string
|
||||||
FolderID int `json:"folderId"`
|
Version uint
|
||||||
FolderTitle string `json:"folderTitle"`
|
HasACL bool
|
||||||
FolderURL string `json:"folderUrl"`
|
IsFolder bool
|
||||||
|
FolderID uint
|
||||||
|
FolderUID string
|
||||||
|
FolderTitle string
|
||||||
|
FolderURL string
|
||||||
|
Provisioned bool
|
||||||
|
ProvisionedExternalID string
|
||||||
|
AnnotationsPermissions AnnotationsPermissions
|
||||||
|
Dashboard any
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardVersion struct {
|
type AnnotationsPermissions struct {
|
||||||
ID uint `json:"id"`
|
Dashboard AnnotationPermissions
|
||||||
DashboardID uint `json:"dashboardId"`
|
Organization AnnotationPermissions
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getRawDashboardByUID(ctx context.Context, path string) ([]byte, BoardProperties, error) {
|
type AnnotationPermissions struct {
|
||||||
raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil)
|
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 {
|
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 body schema.Dashboard
|
||||||
var versionInfo DashboardVersion
|
|
||||||
raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil)
|
resp, err := c.client.do(req, &body)
|
||||||
if err != nil {
|
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)
|
return DashboardFromSchema(body), resp, nil
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryParams(params ...QueryParam) url.Values {
|
type DashboardCreateOpts struct {
|
||||||
u := url.URL{}
|
Dashboard any
|
||||||
q := u.Query()
|
FolderID uint
|
||||||
for _, p := range params {
|
FolderUID string
|
||||||
p(&q)
|
Message string
|
||||||
}
|
Overwrite bool
|
||||||
return q
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetDashboardVersionsByDashboardUID(ctx context.Context, uid string, params ...QueryParam) ([]DashboardVersion, error) {
|
func (o DashboardCreateOpts) Validate() error {
|
||||||
var (
|
if o.Dashboard == nil {
|
||||||
raw []byte
|
return errors.New("dashboard is nil")
|
||||||
code int
|
}
|
||||||
err error
|
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
|
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) {
|
func (c *DashboardClient) ParseRaw(raw []byte) (*Dashboard, error) {
|
||||||
return c.getRawDashboardByUID(ctx, "uid/"+uid)
|
var body schema.Dashboard
|
||||||
}
|
if err := json.Unmarshal(raw, &body); err != nil {
|
||||||
|
return nil, err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.ParseUint(input, 10, 64); 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.ParseUint(input, 10, 64); 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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
SearchParam func(*url.Values)
|
||||||
|
|
||||||
|
SearchType string
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SearchTypeFolder SearchParamType = "dash-folder"
|
SearchTypeFolder SearchType = "dash-folder"
|
||||||
SearchTypeDashboard SearchParamType = "dash-db"
|
SearchTypeDashboard SearchType = "dash-db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FoundBoard struct {
|
func WithType(searchType SearchType) SearchParam {
|
||||||
ID uint `json:"id"`
|
return func(v *url.Values) {
|
||||||
UID string `json:"uid"`
|
v.Set("type", string(searchType))
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
func WithQuery(query string) SearchParam {
|
||||||
// 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 {
|
|
||||||
return func(v *url.Values) {
|
return func(v *url.Values) {
|
||||||
if query != "" {
|
if query != "" {
|
||||||
v.Set("query", 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) {
|
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) {
|
return func(v *url.Values) {
|
||||||
if tag != "" {
|
if tag != "" {
|
||||||
v.Add("tag", tag)
|
v.Add("tag", tag)
|
||||||
|
@ -82,8 +47,59 @@ func SearchTag(tag string) SearchParam {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchType(searchType SearchParamType) SearchParam {
|
type SearchResult struct {
|
||||||
return func(v *url.Values) {
|
ID uint
|
||||||
v.Set("type", string(searchType))
|
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",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:base"
|
"config:base",
|
||||||
|
"docker:enableMajor"
|
||||||
],
|
],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
|
@ -11,5 +12,10 @@
|
||||||
"platformAutomerge": true,
|
"platformAutomerge": true,
|
||||||
"dependencyDashboard": true
|
"dependencyDashboard": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"postUpdateOptions": [
|
||||||
|
"gomodTidy",
|
||||||
|
"gomodUpdateImportPaths"
|
||||||
|
],
|
||||||
|
"semanticCommits": "enabled"
|
||||||
}
|
}
|
Loading…
Reference in a new issue