diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..bce580a --- /dev/null +++ b/.drone.yml @@ -0,0 +1,124 @@ +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 + diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index c6b39dc..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,358 +0,0 @@ -# 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 diff --git a/.woodpecker/.build.yaml b/.woodpecker/.build.yaml deleted file mode 100644 index 60c67b4..0000000 --- a/.woodpecker/.build.yaml +++ /dev/null @@ -1,42 +0,0 @@ -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 diff --git a/.woodpecker/.deploy.yaml b/.woodpecker/.deploy.yaml deleted file mode 100644 index ed45768..0000000 --- a/.woodpecker/.deploy.yaml +++ /dev/null @@ -1,59 +0,0 @@ -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 diff --git a/.woodpecker/.lint.yaml b/.woodpecker/.lint.yaml deleted file mode 100644 index c4f1dc3..0000000 --- a/.woodpecker/.lint.yaml +++ /dev/null @@ -1,20 +0,0 @@ -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 diff --git a/.woodpecker/.test.yaml b/.woodpecker/.test.yaml deleted file mode 100644 index 226568d..0000000 --- a/.woodpecker/.test.yaml +++ /dev/null @@ -1,9 +0,0 @@ -steps: -- name: gotest - image: golang:1.22.5 - commands: - - go test ./... - when: - - event: push -depends_on: - - lint diff --git a/Dockerfile b/Dockerfile index ee958ea..62b802a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.5-bookworm AS build +FROM golang:1.22.4-bookworm AS build # Create build workspace folder WORKDIR /workspace @@ -9,7 +9,7 @@ RUN apt-get update --yes && \ apt-get install --yes build-essential # Build the actual binary -RUN CGO_ENABLED=0 go build -o grafana-backuper cmd/main.go +RUN CGO_ENABLED=0 go build -o grafana-backuper main.go # -- -- -- -- -- -- @@ -20,14 +20,6 @@ FROM alpine WORKDIR /app # Copy built binary from build image -COPY --from=build /workspace/grafana-backuper . +COPY --from=build /workspace/grafana-backuper /app -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"] +ENTRYPOINT ["/app/grafana-backuper"] diff --git a/cfg/cfg.go b/cfg/cfg.go new file mode 100644 index 0000000..b79bdc3 --- /dev/null +++ b/cfg/cfg.go @@ -0,0 +1,91 @@ +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) + } +} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 33d3200..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,22 +0,0 @@ -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) - } -} diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index b5a87a3..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/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 diff --git a/go.mod b/go.mod index 96ddff7..c904a47 100644 --- a/go.mod +++ b/go.mod @@ -1,55 +1,32 @@ module git.ar21.de/yolokube/grafana-backuper -go 1.22.5 +go 1.22.2 require ( github.com/ProtonMail/go-crypto v1.0.0 + github.com/alecthomas/kong v0.9.0 github.com/go-git/go-billy/v5 v5.6.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 ( - dario.cat/mergo v1.0.1 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/cloudflare/circl v1.3.9 // indirect - github.com/cyphar/filepath-securejoin v0.3.1 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/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/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/skeema/knownhosts v1.2.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + golang.org/x/tools v0.18.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cd6e275..5bcb67d 100644 --- a/go.sum +++ b/go.sum @@ -1,53 +1,57 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= +github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= +github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= +github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= -github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= -github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -59,83 +63,48 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -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/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -143,11 +112,17 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -157,12 +132,14 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -170,8 +147,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -179,23 +156,22 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go deleted file mode 100644 index dac2628..0000000 --- a/internal/cmd/backup.go +++ /dev/null @@ -1,58 +0,0 @@ -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) -} diff --git a/internal/cmd/restore.go b/internal/cmd/restore.go deleted file mode 100644 index 247f798..0000000 --- a/internal/cmd/restore.go +++ /dev/null @@ -1,54 +0,0 @@ -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) -} diff --git a/internal/cmd/root.go b/internal/cmd/root.go deleted file mode 100644 index 4d5fb05..0000000 --- a/internal/cmd/root.go +++ /dev/null @@ -1,102 +0,0 @@ -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) - } - } - }) -} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index f95cb6a..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,66 +0,0 @@ -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 -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index 4102d67..0000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,40 +0,0 @@ -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 -} diff --git a/internal/logger/logger.go b/internal/logger/logger.go deleted file mode 100644 index fbcc944..0000000 --- a/internal/logger/logger.go +++ /dev/null @@ -1,28 +0,0 @@ -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() -} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go deleted file mode 100644 index 34a3766..0000000 --- a/internal/logger/logger_test.go +++ /dev/null @@ -1,49 +0,0 @@ -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 -} diff --git a/internal/version/version.go b/internal/version/version.go deleted file mode 100644 index bd65be1..0000000 --- a/internal/version/version.go +++ /dev/null @@ -1,17 +0,0 @@ -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 -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..72162df --- /dev/null +++ b/main.go @@ -0,0 +1,85 @@ +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) + } + } + } +} diff --git a/pkg/git/commit.go b/pkg/git/commit.go deleted file mode 100644 index a4508c1..0000000 --- a/pkg/git/commit.go +++ /dev/null @@ -1,105 +0,0 @@ -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 -} diff --git a/pkg/git/git.go b/pkg/git/git.go new file mode 100644 index 0000000..d696f55 --- /dev/null +++ b/pkg/git/git.go @@ -0,0 +1,240 @@ +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, + }, + ) +} diff --git a/pkg/git/project.go b/pkg/git/project.go deleted file mode 100644 index 21d9bb7..0000000 --- a/pkg/git/project.go +++ /dev/null @@ -1,251 +0,0 @@ -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) -} diff --git a/pkg/git/project_test.go b/pkg/git/project_test.go deleted file mode 100644 index 5f3f92c..0000000 --- a/pkg/git/project_test.go +++ /dev/null @@ -1,14 +0,0 @@ -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) -} diff --git a/pkg/git/sign_key_test.go b/pkg/git/sign_key_test.go deleted file mode 100644 index ef71347..0000000 --- a/pkg/git/sign_key_test.go +++ /dev/null @@ -1,36 +0,0 @@ -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) -} diff --git a/pkg/git/sign_key.go b/pkg/git/signer.go similarity index 52% rename from pkg/git/sign_key.go rename to pkg/git/signer.go index 5ac350b..1834d06 100644 --- a/pkg/git/sign_key.go +++ b/pkg/git/signer.go @@ -7,29 +7,22 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/armor" ) -type SignKey struct { - KeyFile string - - entity *openpgp.Entity -} - -func (s *SignKey) ReadKeyFile() error { - file, err := os.Open(s.KeyFile) +func getSigner(keyFile string) (*openpgp.Entity, error) { + file, err := os.Open(keyFile) if err != nil { - return err + return nil, err } defer file.Close() block, err := armor.Decode(file) if err != nil { - return err + return nil, err } entityList, err := openpgp.ReadKeyRing(block.Body) - if err != nil || len(entityList) < 1 { - return err + if err != nil { + return nil, err } - s.entity = entityList[0] - return nil + return entityList[0], nil } diff --git a/pkg/git/testdata/test-empty-key.asc b/pkg/git/testdata/test-empty-key.asc deleted file mode 100644 index 00e9653..0000000 --- a/pkg/git/testdata/test-empty-key.asc +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: Keybase OpenPGP v1.0.0 -Comment: https://keybase.io/crypto ------END PGP PRIVATE KEY BLOCK----- diff --git a/pkg/git/testdata/test-invalid-key.asc b/pkg/git/testdata/test-invalid-key.asc deleted file mode 100644 index 8d9c88f..0000000 --- a/pkg/git/testdata/test-invalid-key.asc +++ /dev/null @@ -1 +0,0 @@ -invalid content diff --git a/pkg/git/testdata/test-key.asc b/pkg/git/testdata/test-key.asc deleted file mode 100644 index f2fcb86..0000000 --- a/pkg/git/testdata/test-key.asc +++ /dev/null @@ -1,182 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: Keybase OpenPGP v1.0.0 -Comment: https://keybase.io/crypto - -xcaGBGbEwjQBEADOdK3XF76rKhw8no1/+AXbBSJJxq5JP0cykHyTPHhSxbkQliVF -C/dpuvtY+wzY+DRvqjvwemsfwEtJZI06ZkvIbTpWr03M1UT1EDR6HkRIczyJdUq0 -TM/gmcGUBA4Xooiu9xYSLEWJrAoJdw8DSlSmNqxQYLoesU4yx4cYC7NQOqTl/eT+ -fkEqSisxaOnMc1V8d5sXwYeNl+62C4BL4dn8wTc1zJraqLJiRNqpNCHGII+k1pYW -144lARfTyxjRhJ/kqg3l6T0CCShXALPrUsUTi1Fj2Zi/F2sPF6DTg/EjA6fARwYQ -YSLGIaysqHB4HbI0+ythfApmssjuQkodpX4D2ato+Y7cMYvKhGxZbFg0YIw3Nukf -RzADKv1aBdEn5AeSsKSVLH5v+vGPfED919xYlDv6gdAO59JOCHQkz7Y6h7RIGDba -KIwZtL0k9PEeKWcz7s/YG+w7S1VDm6GoPRypqCLNAs4mW0h8HgAiVBqXG4sSIDxb -A7hTGYy4ZZs+8XjO8KkojLd/5341J8Dwufw7pXLJ1VRP/c+3CgpD499YFgxBl8Ax -MXhk/TmXTVZLiAyj4f/R4I1mvGxWcaCFPHiDg6eJCeppvXrt0Sdgxk1hD00gW8CH -XY2AGz3GYA/Iu6HMd9Xlbo5CSHFoGUQZKfLnPU7FZY3u62b2iUT3oHHyfQARAQAB -/gkDCAw71bpmGR4IYL2gFIz8OaCi6ycZAgfeg2ddpc44ViAmbe39zy2h+peWew3N -A8Ds9lPTQWsv2TkYXiUGagsshI6PjwxVXmaVYNcOQDM0yE+hxjT5hSK1fuZuZEWV -pCOsoy9IZbb/M5AarD4BSAiqAaIfJawtTyC363rlBa2uQLFR2r5sCYzTTUXwRA82 -KEBC0Vg8Rcb40Ww24rszPGs0HPnhQ2IMO2RZgn2bh/FLCP34GTYJPtg9JBdx25CI -5Mo8rDaGLgbwkVBDMt66/FK/qtnPPL5yUa1h0WKCGhCTsktUh4b7voanKqtwg9mU -UT3d2x6+VwF0JXL50dj642MdIX7WH+cx/HqNdDagInszGf9JW42QDoPhW5LHz6QJ -mj62vmf+IlqUYKM/k/PUh330eNoTh6arfdLWYUbljcjLxhwKVxmu77bj/jL/e8fO -1eTieu9oi9OD9suatT3aInN5xcnYe7VGhQknOoECVkDGzeP36a9uxL6V90jF/6Sy -QDHPYwhFZ671cAw/mpC5reibRjphmsTZN3Hg4tyfiqPGXi7KQVd0aqsBVSKJaFVM -5TMgUzHha1XD5sls8eYucsQaxysX+VWtRYiJ8kxtFqzy8rxddAshLmvGPbpU869t -jhVAAolkinOyIwzphXMvoGCBF9x/kfDwqv8AddSw+++HYW9MaZjrUgMT6GbvjYcy -cY4eEzb0sHOKosqpOjdziP7q28hrEJJKqPMpGfNwpEDYl4XU7EFkwN50MMrEzJVX -Wle83dDUMByMKZUDCKhM2QCXpVI67ho2NBxHq47G1nZBjc3LjsFZLquIz6+S7WFQ -yiTckifKXnxyQGXkT/MkqoNq42ItLHQteCYbaV0kxvnrKNEYzglXD4znxWYgaGHE -16SNwnB1LqJpCDmLwAeK26ybmTRT7KdQfRAqw1XPEKGBlL0Sv8YcMvyJdInEHAis -ysBmNngtM5IlSeLY574VEVkMP5/ckRyFwpa2MAsZt58VarWLxt7Vjrcsku8RC6ZH -w/twmdT8QZ4KNA7OuG49Ur54r6GL3GY/kignHH0ctDwBBGB15XURq4I+c5XeN+1c -RN+aXLIq1DGgFuE2/tUrEpbveKUdKVRNekZFqR1UDbKeM8ZanDLALAUI1QUsOJaN -JqsXZe7+yHpOUx3+Y4iSh9DDWIIYfVtv8A8eop5+eaZznv6wN9hMqcY387uAEahR -KredXkqvD/XJXWM+uADV7DCF6IpXNHK6/eHgYcTKFK9m1hJWN2LAIYKn+rqHjds2 -SPtQlVya6NuBYMTAnMOAVmF27AjpxQUzpkgHFyoVblEhNHyk6pVT05s+7w5xo9wI -0WwyN6VCG+kBbVYAUWYzZEqeBjrehVJJCyPOSBV7/Gy5u7UMOXbc5kGam6aEvMcv -4fwNCRA5+yzOAcLUSRJlYWL8qzc/nV76YYwf7YpMxtBiRgaa1c5fsyncaeaymkM+ -nafylvt8NSw7mp2Om2HWXdZJur5Yw+gLGW6jU9HRfp5iTx5DplBm3Bf/LNSGPDlE -KgizsogPuURtrnqY+YuqRbIuqsDzeYxX9Kh2mnSlG7X8Sz0NDBKiF4QKixuBh6m3 -loaHbg2b4MGISdMXQ/Y5XjWMqwOZ17NFFeNS+iV7+qAW3jok/tnJP9H1tQ/KR1CF -9mahHZio5bA1af/jM73CUMPoL9N6BfUD4AJBhQnGk8AUnhnUIBAEKQBxOMq6ZIzA -w9JayeDS7PmQPZW5FsV/YlNYetHuS3FLPHmChO6BrFUra1LCPx0H1EbNS01heCBN -dXN0ZXJtYW5uIChPbmx5IGEgdGVzdCBrZXkgZm9yIHN0YXRpYyB0ZXN0cykgPG1h -eC5tdXN0ZXJtYW5uQHRlc3QuY29tPsLBbQQTAQoAFwUCZsTCNAIbLwMLCQcDFQoI -Ah4BAheAAAoJEDjWGY8PcOIDQMoP/AnSWsjk3cXDwmGaD0qaiCt2X54fi2EFuJU9 -7eRdKD2Q7gB3jmhbHKOhJGHWnEfDb1MJpzCOIYryquWuFKzzgfYS86+7Dnr0+967 -fd9RTg1CPzfhSpUsJcgB/3tePWSb96wc8Ga/oBVK1qmegTCDU9PeORlC9osBmaSh -zxJccpg4HKAntNO50YW6kIVhBxz6AWUpVjQUnknAhp9xrxlpdtDfXjlp7oDyQXb6 -AYOmiEnYLs63ANksuqsrvu9ZzNqsgYdQcHWj5k8isNCVslC05jDfsuerrC+xST+5 -lmlPMAwIzeBQSp6snTbl39aV9E/U/hT7DQGbtpH8qcPFGoFt8KcylFP41Dd+7XzX -lUrzDLPhpyQwbgLFtbRzKFPwd03r27Kp8LT7qdzfwPTyncSlWYuVghtbv9Ar1QV2 -0wCshhLl27mVFtJNpA67aPRMPAvgPPqfV63/6D1TWkSwadmVaxL21Bu+SaydcSh6 -lu91RHNX8g/pkEkzHXlW4oSkR83XD5Mgb1g9v5uKKvaf2lC3Bpzg5PtUXU4WJI0j -SjiFFX1p5Cnn/T0LzLwe6w/6rO/v+jSa0+GoHQMwFf7ZYfWVbLtKvy+DUJnQIBBw -Xv3VvUgd5JkUMeKVehChhwNKBSBL2L1zbape4vhCcHFZ65+XAKXNkych7Z3JAiil -xosyRQ+Hx8aGBGbEwjQBEADSEVgNOxOJPt6bZAnGR9KyH48KPBnpF9IuiWkPkSDB -9Agg0tH4PTCpYR5CfpHQCHGIR7hIjnkUpkDAgc6lgmFHJXrIyV8hjqGmwgKUue7y -GlWJtYIbw0bHWzVcbRKs9zoxNEQejcNhOQleEioqV+ZQnUgYb/p/wclzrmVBCs6D -rxbugzSBmX9n9ikufktjbjm9mty+0qlMI5TP9S/mIqFpnd+XrfpbDasd7ExNBWIP -v3n0WCkJN7GZ11e+t6cMmJ5Y0jsAl0GNwLaPZv6g7o6LYjQcoNQfIrsFhh1tdQrg -vgy+4UA2TjKzeo7pA3/jSe9+epJ0dL8scuf1wR9fn761uU215cEjVhiBZ9KjRKy2 -0iv4V6qs6CozdhuauKnxh+v9klylg2+csMvWvMOzdmx/rZTNzMgqDW9u2J/m+49T -KTRDMK6vusqiKzXV+wWFaEA27AN1Wwjk/cIvMGKg2iicKeJBQxSCLVBxOX94M31q -mkGLVkFLrM813JnjTahQoWOhT9hFeTr3BvgCi0nkiltHtbv3ho7ZlZXm9+Wj0Waj -4Mrm3rsSvcwS0sjK0JlXvxy1uKmJHz99fZYXpr3qG/yviJotCpuQxE+9x5hHooM1 -h/c1Fq9IXZDhqkXJU+NUBEpibEOdWU9EEAX+LkjS5L+sdjNLGDTUO3ACkqY6yFZY -vwARAQAB/gkDCEjiw+cI14/kYNL/OaZAzEzPlHMqhJXbPGpcPzZ8a5f8qXak16E2 -CAApsMM0nrvDYMo81bntGbl+wL2s5kq+Pfsdhlcow45CJRS9lPT6PHRwN8q5UwOx -r8dMM+oLU3gqgZpkAZwdqApGAOQZGSc2Nq5DQ9nheJGKkD8vyF2+eCRdUacrl8A0 -UhRLcJnAM9J8uWTpa7qLP705N8A05q/po44w96n4hWWDK8dkGWU1aFLQ7oFZP9uT -oGZnOBx5S4dNmKVRjFqQIefTW4KX8vU5h+XhDxUFduF6UBblIpGtQz1ujCDCmNaQ -9LA0HiW3ysD1CV1OVwoUgIKt+SDN7IdZeYqFDtBzVp5OD/UcLrFA49rpjbV+s5Pm -QJrlAs+EqDG9BWrh+HtJNeGRHVar/I51Nq1VEbeDAsdjtDf5XgpaOqET8z6VYJah -LzAEhcf17077GvaMyX+6jlGsf5j3CYCY9iAfKxe9EjCiDnDyYzceiwEAqj50/uRL -Ah9pxJy+D8n6Kl3hRebEZDuEqu/viMHQEbL1dk9N6pZ0azoTXWl+SEcPUOkyMDHF -5F6vsoEZto4ffV88vR7mrBCQHpGzv4wt6IY0XujZveQ95LBTmSD1nebpXDfxiraJ -viKF7GTrnKh9VN+++U5ursxcWgL88u5u8EYtLCaveq0VlbZsvWba+pKlV/ZSx9es -v2iNMGvamV9VAsxFN2txy2d2sbZLhgtNWUiH0XHVVzHfl7MA0EkvFJO7uL7KbyXk -jS4R1ylT3qzKqy43J2R20OZqcQYHEaMUpPG9ojdHDF1L7DQ04LjPlBNJXxZ2NIuO -SMDJUInzdMWvNcNIUygkisel9RTM97w0Hecb0ThCBiFJSdOv9ruhCHWgrFg+U+jB -20kGRfGUP05RUoHAT5YHh5oWXkifNBj6gqp43Y1TwE6GBkhMWYEBnqWf1JyrnPdC -L8lMZ/OtkWlZPt94hBicGVfsv7vZUWCTdOQqRLN0OWxkzS4Y+/6QNRXtOcY5rBW9 -dEqCxCegJ2mcJ64kWzOBbOgFk1OUjViCOwWfnJhk9CQA75cY8qiRjfXQne1QIESU -8dvw/4CyQPIAN7U/Ir1a7kQkVewU7t0Z4J5tN+qsrpt/3WDnnAXrPlsw5OiDYG3N -QtjYu0sPSTaHrphdkqx9tLjHR1I7IkOOoj70KkFbWCJITYiCSIHLLHkf/NP2QukZ -WGPJdmIXBt+F5hP8Di1zz/HQV7syoIU3nwrGTpuEwdRAxVORK6na9ECXhFr0XB+b -Cs78KA8XAFxOq150nmqG5VTVb5P4k07/A+hc6bc8teZ1ZNOos8gGYAuuq7KJyNuh -D27gjCfwXamE/EezdszXe2PI4SEVB0DnQ/lM1dTLkwey5W8Slkrd6RlZXi1RWaFp -RxeIm1woZUVX3t7PO7+2uIWWr8P6RLhr4e1RNu4q/w0HE0DgEGrZS1W12TX9gpsR -GgjMB5ewRkOuTxOS1FnnShIA9ntL7+mnbx8r9k/b5T+LqqRhutHHJrAFEd/FiAK3 -Yb1QwlWYCYdwehy5RATIN+6fHL76UhZYmhjjflBEsTelqR2YaWAjGs1sSEAxBqUV -uFIwQ7/3ow0yj8YitOq+oE5eRjYBT4z6aZHYvbTcVT2HhaQOjQz7gL9yGUx6H3hw -87mWgiwdu8Hv/GWcfJhbWxf8Ng2qsGlmR9T8pJcx1zPZQMR1JRmH4wZICIFYVwHs -uAbZKFRSxU2YnML7pIiAJAlpby8WDcJAReLKsIlLR1GduE05JUNRYx4ODfwKKIHC -w4QEGAEKAA8FAmbEwjQFCQ8JnAACGy4CKQkQONYZjw9w4gPBXSAEGQEKAAYFAmbE -wjQACgkQSoyo/ADwHivt3A/9ErfT7No2Oo7h6Vok+3gtYr+FAIipg1krxjbMnVhl -g0TxavbjhDVZ68+EpK5V5jDSFQAE5qPu6euQHwnhM+Mk6/eS2YhZlnT5yjgDDrOy -Bc7ELW/M7EzrFq6LVn6Wu+D5SqD4J62/vs9dFnHwDSCP54NNib1SyBbKN5QzNyPt -6LI6bXUdeBFF5MEqBCLFKcGlmAdJUukkd7Pjpx8B4SnHlDHLsvIyHartV98/0cLG -LZzE8RxEwfP6yHQAWXCqx8lyLyyeo+xf3EfjBKUh0fm5j6O8I/CK838t2KnwxTGh -DzkVXfrw/sJP831ntk+/YNmG4O8+Tw3sGeZ047C8Gn/DCKefXq/IaiZIndoq3dqt -CeOq3LtTpO5ojSBhoi45JdAaVqmmpimlIkf6A7t0hvP839imsywP6UMs3DSkEvzU -FwaMACx7UChhf3Fw/DG/TLIj57om6Hcd21i/ih+LilQJEj7vNCmYdzyA4Zu2oHqc -0lIXk7teTdObj8iU/q82ECNqW/4TYMevRLYdi09z6ueoUT1Py8HOH41oW81skI8j -aUlZkjotMRUDsEMwu/ZExPBhNlV/J6Svmade6xBZ+KMbQgEAijeLxus6Bvhjegy0 -Cu/HPo6hEZUNRAvtkKQRlrKKeWEzSUB/9z4rx0cHt4Nn1ShYjUsOsMgMS2GRe8Uv -9b7Y9A//WzW7u+P6Ol7LwO971Y1TQyRKQ8+UoN3C3MDv4W1sSa2JyPO0kmlu/dHP -WHljwshRBPfIMWPItwn4c4A7aEMtIbjtvfz27SN4tB2echvyeWYRI9IJjZq9dinb -ejlZenpUtGJvjq05SJ550YutidPIWNIxn4scjxTmDJMSh+exZ2/b8yfjkpGV5O3b -0cs3qGKWRGFbeLQtsJynHfC8lFumSvmqmK/yjOaZ6tFGtDVklo0DE73jRiuzZkdy -IUlpr8MJe/shDPceCRnqA9stgIrcH3YBgBRu1UTxjJVoOyXblNP/V5oQ4SO7HpiI -BfXkFDPRoh1IQFY9VITZWAhEv9875cqou7Lkfg/pANe9eWi8Zy5Dsyn8yO/ZgKsU -jVwxnbrPYoaYjOXkoX2zfV+DsAjkRuodvjmpz3J2cTsrBcXPFUhZcASbrBzLkWg6 -XXMoWIa5afNUER3XWZO4KYBTmYpxNSBnRey5pE1CZgTcGacunq6oPL+eZ+2c4BW3 -Te7YqDkuOp0FK7ZydlEGWBLe3cEvSiasmXXibH/u0sMvpHR5TZeRrXVjC91qlTAa -h2x5HiC7I1S49trOSvsZcSFQW27+Aa2s1FeIr2/xJbFKa0u193T9RFYyojnSy2tl -zvSzwSGNGe5/cKDc04AMko6jFd+ZhZMIITcQ16r1NyooNY+BkYXHxoYEZsTCNAEQ -AMEvK/2tfVFssdiUjvtK6QkA1YICEnuKiggj3j9toSt9oKVnKahHvVgF3R48Qbeh -Euhr0hLezJGiUmiywoKVICJk/N7jjdCWx/wIOfLpMLPgo/mgW544rbSAl28gCULu -ravBQKc1A1PZW5l15oq2lvpDBQESqUBCgN39gkt43D3NHClVRFUTg+TLEHaHGOMp -j4vJGNNVsiewqbpSZXQcDLoYQhcoBsNOAucGbR0JlSa8M8Q5vXFX2PlSS0e/EhI4 -fvRb5rW2ssC2m7uCgnzhIF4JHqhw/b5zBHzu8OXANt1QM22NXenzyVskdMyn3H23 -1pZruywhZlDVugWRp0NXcisz+rLX7yiHmZBvvNRgLLOfwqxpB+CFl3T+iHa9gCGx -K6fvySdRHjK88B+Fh7DJcJ5SoEwMFCzuxmkMF21pKqyRht2MNLjZ2i/U+XkhvDJx -ard2QXAwtfYcfJAvyuNhHUM6l7QvFwoYMdnywy9XF0Ssy1Ep8FLogRYrnAUzh28A -iwzZ11ja5w1MSWR4QSEq72JNNAgDptLE5hcpeWWQ2mGiv9qT8KkPq3s+6/k+lHQh -w+0rqgiXyRHQmxCQivASy7fiuwUSdnhEzAS8FJtYnEWg1bsX5R2Uce67Miy3Lqws -aS0JGTq5A1Mf3EuypqNy2torKK4o+S2i6ceKaSlszp9XABEBAAH+CQMIzBFa4pB5 -9ZdgoDMaik7rwegLXsRfzk8qLF8TMV6kHRi42OO2HbLYXLBPr7R80KuP0SQJuYIF -5BVhUo9qkeDrf2pDjWw4Ai39Ipv+YsE9+7SPVxQBeUj9BvFNPDj5oEttJEJtd0sr -cmPrZqjjTbQfHOF8VByk87yCpX0kO34jz5YiTnKo17ADS+xjfu7y08HTGsjdkudv -GrdOHK9qOXPoyY+8+OSxUwwv/W6+5GmuOKzvb06rDZait4MV11dDZaJ2lezVQsSa -vAxkRNaTZVW9VbEfVPQlwCFMIaCHdCUCedNZpNlMvZgjgRhKnFYAljkRxB3ucv/Z -H7reZ7JhJkgsPS1lFdddEoXtv2MmK1PkV/aLjPgJIwF5q81wLDU1FmMbTy65/XxM -y1IRCpTVVo3Z+rJeiMJhZhh1nrIebgd0V182Y8tI1RqKn1YrxFJgiRe2OVPkVCGr -yBNXujdVKZGwIf2ifdYL3tSmOSxskN+In++iE/pf1/qw3MwlratfeIffDJ8N452f -SqjkMPmjY3m2ILgR9gbOk8EZYN36+uZ0M6VJWyTFdzKh0Y14vhBjhbcPc+CKir+Z -Z0sYjjXwnXY6X6oURjQUfBgZvOi0vWYRDMg3RcMQ3Tq7WXkDlMAB2zlLQbEYyhEa -GkgzyssbsouVZGEBMb9HDMJyo2xeS59jruv8KnQNN16mhNWt+vaHdv01AOMLTX3q -MQceHRdvXsq2wGMQD4AJq2wdHRaKC6JeHZD/O2XZFHApEdc+5xSq1KKPxrtXBVOt -SoZ/vC8f/sGFlDt8vES4O1ShrfH6FGfIT15nfMYBO/Q+ByKOjRNLC/r7RocUkk9z -zJbhHiSdGiHVUVrS1MSEhDPjyx7J2RmYUoXpjUDzlj9raLKH32zTY1GYtA1w1W4s -Ic/rx+m/frek14qT1bVR2gaOSqLbrHAq4lWDzRvVGp2ns3D2byIAApZkxl2zYq3v -vuozCWOHdS0dzN4CVOg8yRv5ypq6aiGx4b3T0asOElpN4na+7jBcv5+R5E2B//a6 -oX3zU2TEyjBzsLMT/XYGFPkrZD5EyRMOLPuXc0x3TOBqk+85ZtFMR1Tet3d5283+ -0QcsSQbH7qEs9B+dlAHqhvVSZyOuAj97s6SUktX7Qky+rZhS2C16/Wi9nQLRUvM7 -WsF+3BCyLUTudCJVEPFO5xXCNFIqM7DZUvEVy3uV/ZrmpWZlO4nE/J//JUJH6/cC -VqYDK3V275cYzhqiB/nSZe0duj1gFdF/5HO4pAX0MozYSzDlrYMBW8Q3SYxJJJvK -PChrfg+zirXO4NWzKMjmBw7O1KCqOxbjHc+XRaIOwn3JXEXOIUYm7+hmtjV3NEc1 -qVfEIvEKyEbjMm0Tx94GXyOULPlM9tCwFNIes8YBluHg89GETqHkw8EicaH2Qkx4 -sSKMKh/M5s578c3oAL8mWsW5Qnv9I27XkNqGC5tgkvwwofOwPQvUNdWdjECzPd7t -jXlX6JP7xBLBM3YwNuq6OQxPqjNj55RvyUVgAPwIGbiA9MlZQAzWHwy9A0u+LdLt -OP7AKibGOdQrHY5gHqSxcA/OhzJfMVBOTtKrZRJEynTEfJWGoF/hgKDlIZ4jb0Sw -iQYNnVPAnlc+v5x/Y0kFJn/O9DjhSVTdD83Ov+TNGda909fNTD7vmP9naUvkl96T -kR55Rx+EsTvzzEPU60LSDSe+or9hNhneIxDxLbiggwnBsr+6TSB3P+SLSFVLSx9p -IM0YASsu/2tZHitdTp2P9GYiHlowScO8mlTpwp8DhMLDhAQYAQoADwUCZsTCNAUJ -DwmcAAIbLgIpCRA41hmPD3DiA8FdIAQZAQoABgUCZsTCNAAKCRCNoWTYJnq6QbRp -D/41bvYEJzomd8+gqscfsFztLYgiwJA5f/dI9JPvelJ1/kin96sCjvzdiwjMebZS -oXEk4lqaLTf8lm/Is942o4E7IefYJ+7aFebwyBFkv7uWfPeehibdFqSCLxSEz+vj -NT+WD5bjkAmgrIdDckPIu+TjUAZ1Pp5hDzS/xNRTNib1oSjCYrF8Cq7SgIy6ln5x -0neqNUU97WUqkb34igJhJ/LHfBgBHG9o2C5u4xU8S6RSkyQLm24FH9na1Y82Ee2w -JpXS8iyjyKfx+fE47W3BR2pGtvYt0HHP1m74cdYUIJ3jRWkD9N+4WQYLUbYW1T78 -FeYHoo8vN9bkKn34R9u9UEcBdWOm1xYk3ZZ5gE0ssmW9CaQxpwSW2NrFBZwP3F1b -lialkz5tmkIBge+gmgOfRI9f/mlkNn2no/mhf1+fSjb9TeuSQmOCwweyPBE0qv96 -/8U99C4ROUcvv0pF+NuDPPlHrLpTdrsf9fotg7sS/Kcs8o5M7ifKcOh7cfX5eSxE -CM29bJSrtcIiWqsaS2+aal5wBi+DF80PWsx/RJopDZjZlGJ1VqAekfugeTtQOt9D -IpUN4NjZ6p5cWoJlnNyQtAjwCYTt9CrRwRJ1DV0dOsekm3BrD/Tc/D2gxwHqR27l -wgS6IfsW4egRV5HoX+9dbeJP2M47mKoYeW34Vcy7D4nKU99gD/90Cp0137DJKWAG -amh1TApxge5MvdMA+xiR2334U8EvyWgVJugFKdDnoGHVNCMdLlh2qnWiru6WCT35 -ASxyvhM3Hv1yrKhr5TTU5P0uKH4PJ1NdCaOv4jYzffQnlrBAO4U+LzEHuIWKKFCE -30YpSrd0GD0SvufSkgl7kJm2DlxhGX1wwFBUZGvSSsibO63tvXnFlJeKKMacS6in -Qc/TkF2BPm4kPAJVl4nxWTgS8DtMm12T/J5M+s1+IjX6pmkKct6QYZDzYdvpxCLo -X9Gbz+uRb5P1SkKjBDbD/SNWB3W0HsPZpFdtikUN/UdEIN3ZBIgKiMnhRffx+Syb -EkPTIvnCsEW65XTCwLYkLdVqNQcphwttnC0j3DluoRbaVeZFR3iN53tIm8n1uO0N -5fTapgBmwc1gGGgPhunmVXJVOAnKtvKmXWyJKBsbmFNLO8IX0759sU72fsGwFVVO -WX2+AKt5lnMtqF+b2YEjakgcaIMuZ+2VlXPAgPeH7wxl8JdJZ72joZDWnqa7cAc8 -eXxEOnMeVMdt44cRwNZeT1Ic+TqRfUvJFFCFTc9daoR/O7qQbLY+47oIbex9Ffjv -pWsytOctoe93RUxV/saRti06WlW1aQdMCTINUWA30rFs3KyM09r0p1BEu0rYx9nb -YZ3g9Y8QOzszYPB450rVxLOEY6eV1Q== -=W3yz ------END PGP PRIVATE KEY BLOCK----- diff --git a/pkg/grafana/client.go b/pkg/grafana/client.go deleted file mode 100644 index ee4ab86..0000000 --- a/pkg/grafana/client.go +++ /dev/null @@ -1,143 +0,0 @@ -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 -} diff --git a/pkg/grafana/dashboard.go b/pkg/grafana/dashboard.go index 666be85..d501ad7 100644 --- a/pkg/grafana/dashboard.go +++ b/pkg/grafana/dashboard.go @@ -4,147 +4,134 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" + "net/url" "time" - - "git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema" ) -type Dashboard struct { - IsStarred bool - Type string - CanSave bool - CanEdit bool - CanAdmin bool - CanStar bool - CanDelete bool - Slug string - URL string - Expires time.Time - Created time.Time - Updated time.Time - UpdatedBy string - CreatedBy string - Version uint - HasACL bool - IsFolder bool - FolderID uint - FolderUID string - FolderTitle string - FolderURL string - Provisioned bool - ProvisionedExternalID string - AnnotationsPermissions AnnotationsPermissions - Dashboard any +type BoardProperties struct { + IsStarred bool `json:"isStarred,omitempty"` + IsHome bool `json:"isHome,omitempty"` + IsSnapshot bool `json:"isSnapshot,omitempty"` + Type string `json:"type,omitempty"` + CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` + CanStar bool `json:"canStar"` + Slug string `json:"slug"` + Expires time.Time `json:"expires"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + UpdatedBy string `json:"updatedBy"` + CreatedBy string `json:"createdBy"` + Version int `json:"version"` + FolderID int `json:"folderId"` + FolderTitle string `json:"folderTitle"` + FolderURL string `json:"folderUrl"` } -type AnnotationsPermissions struct { - Dashboard AnnotationPermissions - Organization AnnotationPermissions +type DashboardVersion struct { + ID uint `json:"id"` + DashboardID uint `json:"dashboardId"` + DashboardUID string `json:"uid"` + ParentVersion uint `json:"parentVersion"` + RestoredFrom uint `json:"restoredFrom"` + Version uint `json:"version"` + Created time.Time `json:"created"` + CreatedBy string `json:"createdBy"` + Message string `json:"message"` } -type AnnotationPermissions struct { - CanAdd bool - CanEdit bool - CanDelete bool -} - -type DashboardClient struct { - client *Client -} - -func (c *DashboardClient) Get(ctx context.Context, uid string) (*Dashboard, *Response, error) { - req, err := c.client.newRequest(ctx, "GET", fmt.Sprintf("/dashboards/uid/%s", uid), nil) +func (c *Client) getRawDashboardByUID(ctx context.Context, path string) ([]byte, BoardProperties, error) { + raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil) if err != nil { - return nil, nil, err + return nil, BoardProperties{}, err } - - var body schema.Dashboard - - resp, err := c.client.do(req, &body) - if err != nil { - return nil, resp, err + if code != 200 { + return raw, BoardProperties{}, fmt.Errorf("HTTP error %d: returns %s", code, raw) } - - return DashboardFromSchema(body), resp, nil + 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 } -type DashboardCreateOpts struct { - Dashboard any - FolderID uint - FolderUID string - Message string - Overwrite bool +func (c *Client) getRawDashboardFromVersion(ctx context.Context, path string) ([]byte, DashboardVersion, error) { + var versionInfo DashboardVersion + raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil) + if err != nil { + return nil, versionInfo, err + } + if code != 200 { + return raw, versionInfo, fmt.Errorf("HTTP error %d: returns %s", code, raw) + } + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.UseNumber() + if err := dec.Decode(&versionInfo); err != nil { + return raw, versionInfo, fmt.Errorf("failed unmarshalling dashboard from path %s: %v", path, err) + } + return raw, versionInfo, nil } -func (o DashboardCreateOpts) Validate() error { - if o.Dashboard == nil { - return errors.New("dashboard is nil") +func queryParams(params ...QueryParam) url.Values { + u := url.URL{} + q := u.Query() + for _, p := range params { + p(&q) } - return nil + return q } -type DashboardCreateResponse struct { - DashboardID uint - DashboardUID string - URL string - Status string - Version uint - Slug string -} +func (c *Client) GetDashboardVersionsByDashboardUID(ctx context.Context, uid string, params ...QueryParam) ([]DashboardVersion, error) { + var ( + raw []byte + code int + err error + ) -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 { + if raw, code, err = c.get(ctx, fmt.Sprintf("api/dashboards/uid/%s/versions", uid), queryParams(params...)); err != nil { return nil, err } + if code != 200 { + return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw) + } + var versions []DashboardVersion + err = json.Unmarshal(raw, &versions) - return c.client.do(req, nil) + return versions, err } -func (c *DashboardClient) ParseRaw(raw []byte) (*Dashboard, error) { - var body schema.Dashboard - if err := json.Unmarshal(raw, &body); err != nil { - return nil, err +func (c *Client) GetRawDashboardByUID(ctx context.Context, uid string) ([]byte, BoardProperties, error) { + return c.getRawDashboardByUID(ctx, "uid/"+uid) +} + +func (c *Client) GetRawDashboardByUIDAndVersion(ctx context.Context, uid string, version uint) ([]byte, DashboardVersion, error) { + return c.getRawDashboardFromVersion(ctx, "uid/"+uid+"/versions/"+fmt.Sprint(version)) +} + +func (c *Client) SearchDashboards(ctx context.Context, query string, starred bool, tags ...string) ([]FoundBoard, error) { + params := []SearchParam{ + SearchType(SearchTypeDashboard), + SearchQuery(query), + SearchStarred(starred), + } + for _, tag := range tags { + params = append(params, SearchTag(tag)) + } + return c.Search(ctx, params...) +} + +func ConvertRawToIndent(raw []byte) (string, error) { + var buf bytes.Buffer + + err := json.Indent(&buf, raw, "", " ") + if err != nil { + return "", fmt.Errorf("error pritty-printing raw json string: %v", err) } - return DashboardFromSchema(body), nil + return buf.String(), nil } diff --git a/pkg/grafana/dashboard_version.go b/pkg/grafana/dashboard_version.go deleted file mode 100644 index d3c6a96..0000000 --- a/pkg/grafana/dashboard_version.go +++ /dev/null @@ -1,183 +0,0 @@ -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 -} diff --git a/pkg/grafana/requests.go b/pkg/grafana/requests.go new file mode 100644 index 0000000..d9f9c42 --- /dev/null +++ b/pkg/grafana/requests.go @@ -0,0 +1,75 @@ +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) +} diff --git a/pkg/grafana/schema.go b/pkg/grafana/schema.go deleted file mode 100644 index afed52c..0000000 --- a/pkg/grafana/schema.go +++ /dev/null @@ -1,141 +0,0 @@ -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, - } -} diff --git a/pkg/grafana/schema/dashboard.go b/pkg/grafana/schema/dashboard.go deleted file mode 100644 index c6207fb..0000000 --- a/pkg/grafana/schema/dashboard.go +++ /dev/null @@ -1,63 +0,0 @@ -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"` -} diff --git a/pkg/grafana/schema/dashboard_version.go b/pkg/grafana/schema/dashboard_version.go deleted file mode 100644 index 45f2643..0000000 --- a/pkg/grafana/schema/dashboard_version.go +++ /dev/null @@ -1,18 +0,0 @@ -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 diff --git a/pkg/grafana/schema/error.go b/pkg/grafana/schema/error.go deleted file mode 100644 index f3f06a0..0000000 --- a/pkg/grafana/schema/error.go +++ /dev/null @@ -1,6 +0,0 @@ -package schema - -type ErrorResponse struct { - Message string `json:"message"` - TraceID uint `json:"traceid"` -} diff --git a/pkg/grafana/schema/search.go b/pkg/grafana/schema/search.go deleted file mode 100644 index eef07e2..0000000 --- a/pkg/grafana/schema/search.go +++ /dev/null @@ -1,20 +0,0 @@ -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 diff --git a/pkg/grafana/search.go b/pkg/grafana/search.go index 16912c2..94dd1ea 100644 --- a/pkg/grafana/search.go +++ b/pkg/grafana/search.go @@ -2,30 +2,65 @@ package grafana import ( "context" + "encoding/json" + "fmt" "net/url" "strconv" - - "git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema" -) - -type ( - SearchParam func(*url.Values) - - SearchType string ) const ( - SearchTypeFolder SearchType = "dash-folder" - SearchTypeDashboard SearchType = "dash-db" + SearchTypeFolder SearchParamType = "dash-folder" + SearchTypeDashboard SearchParamType = "dash-db" ) -func WithType(searchType SearchType) SearchParam { - return func(v *url.Values) { - v.Set("type", string(searchType)) - } +type FoundBoard struct { + ID uint `json:"id"` + UID string `json:"uid"` + Title string `json:"title"` + URI string `json:"uri"` + URL string `json:"url"` + Slug string `json:"slug"` + Type string `json:"type"` + Tags []string `json:"tags"` + IsStarred bool `json:"isStarred"` + FolderID int `json:"folderId"` + FolderUID string `json:"folderUid"` + FolderTitle string `json:"folderTitle"` + FolderURL string `json:"folderUrl"` } -func WithQuery(query string) SearchParam { +type ( + // SearchParam is a type for specifying Search params. + SearchParam func(*url.Values) + // SearchParamType is a type accepted by SearchType func. + SearchParamType string + // QueryParam is a type for specifying arbitrary API parameters + QueryParam func(*url.Values) +) + +func (c *Client) Search(ctx context.Context, params ...SearchParam) ([]FoundBoard, error) { + var ( + raw []byte + boards []FoundBoard + code int + err error + ) + u := url.URL{} + q := u.Query() + for _, p := range params { + p(&q) + } + if raw, code, err = c.get(ctx, "api/search", q); err != nil { + return nil, err + } + if code != 200 { + return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw) + } + err = json.Unmarshal(raw, &boards) + return boards, err +} + +func SearchQuery(query string) SearchParam { return func(v *url.Values) { if query != "" { v.Set("query", query) @@ -33,13 +68,13 @@ func WithQuery(query string) SearchParam { } } -func WithStarred() SearchParam { +func SearchStarred(starred bool) SearchParam { return func(v *url.Values) { - v.Set("starred", strconv.FormatBool(true)) + v.Set("starred", strconv.FormatBool(starred)) } } -func WithTag(tag string) SearchParam { +func SearchTag(tag string) SearchParam { return func(v *url.Values) { if tag != "" { v.Add("tag", tag) @@ -47,59 +82,8 @@ func WithTag(tag string) SearchParam { } } -type SearchResult struct { - ID uint - UID string - Title string - URI string - URL string - Slug string - Type string - Tags []string - IsStarred bool - SortMeta uint - FolderID int - FolderUID string - FolderTitle string - FolderURL string -} - -type SearchClient struct { - client *Client -} - -func (c *SearchClient) Search(ctx context.Context, params ...SearchParam) ([]*SearchResult, *Response, error) { - searchURL, err := url.Parse("/search") - if err != nil { - return nil, nil, err - } - - if len(params) > 0 { - query := searchURL.Query() - - for _, param := range params { - param(&query) - } - - searchURL.RawQuery = query.Encode() - } - - req, err := c.client.newRequest(ctx, "GET", searchURL.String(), nil) - if err != nil { - return nil, nil, err - } - - var body schema.SearchResultListResponse - - resp, err := c.client.do(req, &body) - if err != nil { - return nil, resp, err - } - - searchResults := make([]*SearchResult, 0, len(body)) - for _, searchResult := range body { - searchResults = append(searchResults, SearchResultFromSchema(searchResult)) - } - - return searchResults, resp, nil +func SearchType(searchType SearchParamType) SearchParam { + return func(v *url.Values) { + v.Set("type", string(searchType)) + } } diff --git a/pkg/grafanabackuper/backup.go b/pkg/grafanabackuper/backup.go deleted file mode 100644 index 67fc63b..0000000 --- a/pkg/grafanabackuper/backup.go +++ /dev/null @@ -1,159 +0,0 @@ -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") -} diff --git a/pkg/grafanabackuper/client.go b/pkg/grafanabackuper/client.go deleted file mode 100644 index 5fda218..0000000 --- a/pkg/grafanabackuper/client.go +++ /dev/null @@ -1,86 +0,0 @@ -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() -} diff --git a/pkg/grafanabackuper/message.go b/pkg/grafanabackuper/message.go deleted file mode 100644 index 66a364b..0000000 --- a/pkg/grafanabackuper/message.go +++ /dev/null @@ -1,22 +0,0 @@ -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 -} diff --git a/pkg/grafanabackuper/restore.go b/pkg/grafanabackuper/restore.go deleted file mode 100644 index d6576d4..0000000 --- a/pkg/grafanabackuper/restore.go +++ /dev/null @@ -1,103 +0,0 @@ -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 -} diff --git a/renovate.json b/renovate.json index 9dcb565..98cd62a 100644 --- a/renovate.json +++ b/renovate.json @@ -1,8 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base", - "docker:enableMajor" + "config:base" ], "packageRules": [ { @@ -12,10 +11,5 @@ "platformAutomerge": true, "dependencyDashboard": true } - ], - "postUpdateOptions": [ - "gomodTidy", - "gomodUpdateImportPaths" - ], - "semanticCommits": "enabled" + ] } \ No newline at end of file