From 886c9dffa865ef70dc677ec49c728aea14f9de74 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 3 Jul 2024 00:10:03 +0000 Subject: [PATCH 01/26] chore(deps): update golang docker tag to v1.22.5 --- .drone.yml | 4 ++-- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index bce580a..b332bac 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,7 +3,7 @@ name: build steps: - name: gofmt - image: golang:1.22.4 + image: golang:1.22.5 commands: - gofmt -l -s . when: @@ -17,7 +17,7 @@ steps: event: - push - name: vuln-check - image: golang:1.22.4 + image: golang:1.22.5 commands: - go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck ./... diff --git a/Dockerfile b/Dockerfile index 62b802a..a157fdf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.4-bookworm AS build +FROM golang:1.22.5-bookworm AS build # Create build workspace folder WORKDIR /workspace From 20a47d9abcaf0994b5b3118fd7cce7c0c8e2c8b5 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Wed, 19 Jun 2024 22:07:51 +0200 Subject: [PATCH 02/26] style: make linter happy --- cfg/cfg.go | 18 +++++++++++++----- main.go | 1 + pkg/git/git.go | 1 + pkg/grafana/dashboard.go | 8 ++++++++ pkg/grafana/requests.go | 9 ++++++++- pkg/grafana/search.go | 3 +++ 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/cfg/cfg.go b/cfg/cfg.go index b79bdc3..dac8eb7 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -43,7 +43,7 @@ func Parse() *AppSettings { ) validateFlags(ctx) - settings := &AppSettings{ + return &AppSettings{ Force: cliStruct.Force, GrafanaURL: cliStruct.GrafanaURL, GrafanaToken: cliStruct.GrafanaToken, @@ -54,34 +54,42 @@ func Parse() *AppSettings { GitPass: cliStruct.GitPass, GPGKey: cliStruct.GPGKey, } - return settings } func validateFlags(cliCtx *kong.Context) { - var flagsValid = true - var messages = []string{} + flagsValid := true + 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) + if err := cliCtx.PrintUsage(false); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } fmt.Println() for i := 0; i < len(messages); i++ { fmt.Println(messages[i]) diff --git a/main.go b/main.go index 72162df..fd8c1bf 100644 --- a/main.go +++ b/main.go @@ -73,6 +73,7 @@ func main() { 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) } diff --git a/pkg/git/git.go b/pkg/git/git.go index d696f55..b2b5df1 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -110,6 +110,7 @@ func (p *Payload) GetRepo(repoURL, user, password string) (err error) { func (p *Payload) IsVersionCommitted(branch string) bool { refName := plumbing.NewBranchReferenceName(branch) + ref, err := p.Repository.Reference(refName, false) if err != nil { return false diff --git a/pkg/grafana/dashboard.go b/pkg/grafana/dashboard.go index d501ad7..8b736d1 100644 --- a/pkg/grafana/dashboard.go +++ b/pkg/grafana/dashboard.go @@ -49,11 +49,14 @@ func (c *Client) getRawDashboardByUID(ctx context.Context, path string) ([]byte, if code != 200 { return raw, BoardProperties{}, fmt.Errorf("HTTP error %d: returns %s", code, raw) } + var result struct { Meta BoardProperties `json:"meta"` } + dec := json.NewDecoder(bytes.NewReader(raw)) dec.UseNumber() + if err := dec.Decode(&result); err != nil { return raw, BoardProperties{}, fmt.Errorf("failed unmarshalling dashboard from path %s: %v", path, err) } @@ -62,6 +65,7 @@ func (c *Client) getRawDashboardByUID(ctx context.Context, path string) ([]byte, 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 @@ -69,8 +73,10 @@ func (c *Client) getRawDashboardFromVersion(ctx context.Context, path string) ([ 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) } @@ -80,6 +86,7 @@ func (c *Client) getRawDashboardFromVersion(ctx context.Context, path string) ([ func queryParams(params ...QueryParam) url.Values { u := url.URL{} q := u.Query() + for _, p := range params { p(&q) } @@ -99,6 +106,7 @@ func (c *Client) GetDashboardVersionsByDashboardUID(ctx context.Context, uid str if code != 200 { return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw) } + var versions []DashboardVersion err = json.Unmarshal(raw, &versions) diff --git a/pkg/grafana/requests.go b/pkg/grafana/requests.go index d9f9c42..176060d 100644 --- a/pkg/grafana/requests.go +++ b/pkg/grafana/requests.go @@ -20,13 +20,14 @@ type Client struct { } 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 + basicAuth := strings.Contains(authString, ":") + if len(authString) > 0 { if !basicAuth { key = fmt.Sprintf("Bearer %s", authString) @@ -47,24 +48,30 @@ func NewClient(apiURL, authString string, client *http.Client) (*Client, error) 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 diff --git a/pkg/grafana/search.go b/pkg/grafana/search.go index 94dd1ea..7303a2a 100644 --- a/pkg/grafana/search.go +++ b/pkg/grafana/search.go @@ -47,15 +47,18 @@ func (c *Client) Search(ctx context.Context, params ...SearchParam) ([]FoundBoar ) 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 } From cfcf3c6c2bad918ac5401f49cc3601db0daa414e Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sun, 23 Jun 2024 02:35:24 +0200 Subject: [PATCH 03/26] fix(golangci): add golangci config --- .drone.yml | 2 +- .golangci.yml | 342 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 .golangci.yml diff --git a/.drone.yml b/.drone.yml index b332bac..30aa472 100644 --- a/.drone.yml +++ b/.drone.yml @@ -12,7 +12,7 @@ steps: - name: golangci-linter image: golangci/golangci-lint:v1.59.1 commands: - - golangci-lint run --enable-all ./... + - golangci-lint run ./... when: event: - push diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..147fb08 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,342 @@ +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 5m + + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + + exhaustruct: + # List of regular expressions to exclude struct packages and their names from checks. + # Regular expressions must match complete canonical struct package/name/structname. + # Default: [] + exclude: + # std libs + - "^net/http.Client$" + - "^net/http.Cookie$" + - "^net/http.Request$" + - "^net/http.Response$" + - "^net/http.Server$" + - "^net/http.Transport$" + - "^net/url.URL$" + - "^os/exec.Cmd$" + - "^reflect.StructField$" + # public libs + - "^github.com/Shopify/sarama.Config$" + - "^github.com/Shopify/sarama.ProducerMessage$" + - "^github.com/mitchellh/mapstructure.DecoderConfig$" + - "^github.com/prometheus/client_golang/.+Opts$" + - "^github.com/spf13/cobra.Command$" + - "^github.com/spf13/cobra.CompletionOptions$" + - "^github.com/stretchr/testify/mock.Mock$" + - "^github.com/testcontainers/testcontainers-go.+Request$" + - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" + - "^golang.org/x/tools/go/analysis.Analyzer$" + - "^google.golang.org/protobuf/.+Options$" + - "^gopkg.in/yaml.v3.Node$" + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + # Ignore comments when counting lines. + # Default false + ignore-comments: true + + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 20 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/gofrs/uuid/v5 + reason: "gofrs' package was not go module before v5" + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + + inamedparam: + # Skips check for interface methods with only a single parameter. + # Default: false + skip-single-param: true + + mnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - args.Error + - flag.Arg + - flag.Duration.* + - flag.Float.* + - flag.Int.* + - flag.Uint.* + - os.Chmod + - os.Mkdir.* + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets.* + - prometheus.LinearBuckets + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, lll ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + perfsprint: + # Optimizes into strings concatenation. + # Default: true + strconcat: false + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + sloglint: + # Enforce not using global loggers. + # Values: + # - "": disabled + # - "all": report all global loggers + # - "default": report only the default slog logger + # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global + # Default: "" + no-global: "all" + # Enforce using methods that accept a context. + # Values: + # - "": disabled + # - "all": report all contextless calls + # - "scope": report only if a context exists in the scope of the outermost function + # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only + # Default: "" + context: "scope" + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + +linters: + disable-all: true + enable: + ## enabled by default + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + ## disabled by default + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - canonicalheader # checks whether net/http.Header uses canonical header + - copyloopvar # detects places where loop variables are copied + - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - exhaustive # checks exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - fatcontext # detects nested contexts in loops + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - gochecknoglobals # checks that no global variables exist + - gochecknoinits # checks that no init functions are present in Go code + - gochecksumtype # checks exhaustiveness on Go "sum types" + - gocognit # computes and checks the cognitive complexity of functions + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocyclo # computes and checks the cyclomatic complexity of functions + - godot # checks if comments end in a period + - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - intrange # finds places where for loops could make use of an integer range + - lll # reports long lines + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - mirror # reports wrong mirror patterns of bytes/strings usage + - misspell # finds commonly misspelled English words in comments + - mnd # detects magic numbers + - musttag # enforces field tags in (un)marshaled structs + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nonamedreturns # reports all named returns + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - protogetter # reports direct reads from proto message fields when getters should be used + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sloglint # ensure consistent code style when using log/slog + - spancheck # checks for mistakes with OpenTelemetry/Census spans + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - stylecheck # is a replacement for golint + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + - testableexamples # checks if examples are testable (have an expected output) + - testifylint # checks usage of github.com/stretchr/testify + - testpackage # makes you use a separate _test package + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace + + ## you may want to enable + #- decorder # checks declaration order and count of types, constants, variables and functions + #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized + #- gci # controls golang package import order and makes it always deterministic + #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega + #- godox # detects FIXME, TODO and other comment keywords + #- goheader # checks is file header matches to pattern + #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters + #- interfacebloat # checks the number of methods inside an interface + #- ireturn # accept interfaces, return concrete types + #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + #- tagalign # checks that struct tags are well aligned + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # checks that errors returned from external packages are wrapped + #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event + + ## disabled + #- containedctx # detects struct contained context.Context field + #- contextcheck # [too many false positives] checks the function whether use a non-inherited context + #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages + #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- dupword # [useless without config] checks for duplicate words in the source code + #- err113 # [too strict] checks the errors handling expressions + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- gofmt # [replaced by goimports] checks whether code was gofmt-ed + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed + #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase + #- grouper # analyzes expression groups + #- importas # enforces consistent import aliases + #- maintidx # measures the maintainability index of each function + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # checks the struct tags + #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + + exclude-rules: + - source: "(noinspection|TODO)" + linters: [ godot ] + - source: "//noinspection" + linters: [ gocritic ] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck From 7ce90dacc44d6ee810ff27eabcca1ca33cca953d Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sun, 23 Jun 2024 02:37:22 +0200 Subject: [PATCH 04/26] refactor(grafana): rework grafana package and make it more modular --- go.mod | 3 +- go.sum | 34 ++-- pkg/grafana/client.go | 143 +++++++++++++++ pkg/grafana/dashboard.go | 229 ++++++++++++------------ pkg/grafana/dashboard_version.go | 163 +++++++++++++++++ pkg/grafana/requests.go | 82 --------- pkg/grafana/schema.go | 98 ++++++++++ pkg/grafana/schema/dashboard.go | 59 ++++++ pkg/grafana/schema/dashboard_version.go | 20 +++ pkg/grafana/schema/error.go | 6 + pkg/grafana/schema/search.go | 22 +++ pkg/grafana/search.go | 133 +++++++------- 12 files changed, 710 insertions(+), 282 deletions(-) create mode 100644 pkg/grafana/client.go create mode 100644 pkg/grafana/dashboard_version.go delete mode 100644 pkg/grafana/requests.go create mode 100644 pkg/grafana/schema.go create mode 100644 pkg/grafana/schema/dashboard.go create mode 100644 pkg/grafana/schema/dashboard_version.go create mode 100644 pkg/grafana/schema/error.go create mode 100644 pkg/grafana/schema/search.go diff --git a/go.mod b/go.mod index 81a1762..f25a302 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/alecthomas/kong v0.9.0 github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 + golang.org/x/net v0.22.0 ) require ( @@ -25,8 +26,8 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.15.0 // indirect - golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.18.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index b1688f7..8b762db 100644 --- a/go.sum +++ b/go.sum @@ -5,14 +5,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc 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/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 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/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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= @@ -30,16 +28,14 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcej github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -69,20 +65,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.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/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= @@ -91,8 +83,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -106,8 +96,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -128,8 +116,6 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -137,8 +123,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.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 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= diff --git a/pkg/grafana/client.go b/pkg/grafana/client.go new file mode 100644 index 0000000..3f973e8 --- /dev/null +++ b/pkg/grafana/client.go @@ -0,0 +1,143 @@ +package grafana + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema" + "golang.org/x/net/http/httpguts" +) + +const UserAgent = "grafana-backuper" + +type Client struct { + endpoint string + token string + tokenValid bool + userAgent string + httpClient *http.Client + + Dashboard DashboardClient + DashboardVersion DashboardVersionClient + File SearchClient +} + +type ClientOption func(*Client) + +func WithToken(token string) ClientOption { + return func(client *Client) { + client.token = token + client.tokenValid = httpguts.ValidHeaderFieldName(token) + } +} + +func WithHTTPClient(httpClient *http.Client) ClientOption { + return func(client *Client) { + client.httpClient = httpClient + } +} + +func WithUserAgent(userAgent string) ClientOption { + return func(client *Client) { + client.userAgent = userAgent + } +} + +func NewClient(endpoint string, options ...ClientOption) *Client { + client := &Client{ + endpoint: endpoint, + tokenValid: true, + httpClient: &http.Client{}, + userAgent: UserAgent, + } + + for _, option := range options { + option(client) + } + + client.Dashboard = DashboardClient{client: client} + client.DashboardVersion = DashboardVersionClient{client: client} + client.File = SearchClient{client: client} + + return client +} + +func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { + url := fmt.Sprintf("%s/%s", c.endpoint, path) + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", c.userAgent) + + if !c.tokenValid { + return nil, errors.New("authorization token contains invalid characters") + } + + if c.token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req.WithContext(ctx), nil +} + +type Response struct { + *http.Response + ErrorResponse *ErrorResponse + + body []byte +} + +type ErrorResponse struct { + Message string + TraceID uint +} + +func (c *Client) Do(req *http.Request, v any) (*Response, error) { + httpResp, err := c.httpClient.Do(req) + resp := &Response{Response: httpResp} + if err != nil { + return resp, err + } + defer httpResp.Body.Close() + + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp, err + } + resp.body = body + + resp.Body = io.NopCloser(bytes.NewReader(body)) + + if resp.StatusCode != http.StatusOK { + var errorResponse schema.ErrorResponse + if err = json.Unmarshal(resp.body, &errorResponse); err == nil { + return resp, fmt.Errorf( + "grafana: got error with status code %d: %s", + resp.StatusCode, + ErrorResponseFromSchema(errorResponse).Message, + ) + } + return resp, fmt.Errorf("grafana: server responded with an unexpected status code %d", resp.StatusCode) + } + + if v != nil { + if writer, ok := v.(io.Writer); ok { + _, err = io.Copy(writer, bytes.NewReader(resp.body)) + } else { + err = json.Unmarshal(resp.body, v) + } + } + + return resp, err +} diff --git a/pkg/grafana/dashboard.go b/pkg/grafana/dashboard.go index 8b736d1..2fa27a5 100644 --- a/pkg/grafana/dashboard.go +++ b/pkg/grafana/dashboard.go @@ -4,142 +4,141 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" - "net/url" "time" + + "git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema" ) -type BoardProperties struct { - IsStarred bool `json:"isStarred,omitempty"` - IsHome bool `json:"isHome,omitempty"` - IsSnapshot bool `json:"isSnapshot,omitempty"` - Type string `json:"type,omitempty"` - CanSave bool `json:"canSave"` - CanEdit bool `json:"canEdit"` - CanStar bool `json:"canStar"` - Slug string `json:"slug"` - Expires time.Time `json:"expires"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - UpdatedBy string `json:"updatedBy"` - CreatedBy string `json:"createdBy"` - Version int `json:"version"` - FolderID int `json:"folderId"` - FolderTitle string `json:"folderTitle"` - FolderURL string `json:"folderUrl"` +type DashboardMeta struct { + IsStarred bool + Type string + CanSave bool + CanEdit bool + CanAdmin bool + CanStar bool + CanDelete bool + Slug string + URL string + Expires time.Time + Created time.Time + Updated time.Time + UpdatedBy string + CreatedBy string + Version uint + HasACL bool + IsFolder bool + FolderID uint + FolderUID string + FolderTitle string + FolderURL string + Provisioned bool + ProvisionedExternalID string + AnnotationsPermissions AnnotationsPermissions + Dashboard any } -type DashboardVersion struct { - ID uint `json:"id"` - DashboardID uint `json:"dashboardId"` - DashboardUID string `json:"uid"` - ParentVersion uint `json:"parentVersion"` - RestoredFrom uint `json:"restoredFrom"` - Version uint `json:"version"` - Created time.Time `json:"created"` - CreatedBy string `json:"createdBy"` - Message string `json:"message"` +type AnnotationsPermissions struct { + Dashboard AnnotationPermissions + Organization AnnotationPermissions } -func (c *Client) getRawDashboardByUID(ctx context.Context, path string) ([]byte, BoardProperties, error) { - raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil) +type AnnotationPermissions struct { + CanAdd bool + CanEdit bool + CanDelete bool +} + +type DashboardClient struct { + client *Client +} + +func (c *DashboardClient) Get(ctx context.Context, uid string) (*DashboardMeta, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/dashboards/uid/%s", uid), nil) if err != nil { - return nil, BoardProperties{}, err - } - if code != 200 { - return raw, BoardProperties{}, fmt.Errorf("HTTP error %d: returns %s", code, raw) + return nil, nil, err } - var result struct { - Meta BoardProperties `json:"meta"` - } + var body schema.DashboardMeta - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.UseNumber() - - if err := dec.Decode(&result); err != nil { - return raw, BoardProperties{}, fmt.Errorf("failed unmarshalling dashboard from path %s: %v", path, err) - } - return raw, result.Meta, nil -} - -func (c *Client) getRawDashboardFromVersion(ctx context.Context, path string) ([]byte, DashboardVersion, error) { - var versionInfo DashboardVersion - - raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil) + resp, err := c.client.Do(req, &body) if err != nil { - return nil, versionInfo, err - } - if code != 200 { - return raw, versionInfo, fmt.Errorf("HTTP error %d: returns %s", code, raw) + return nil, resp, err } - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.UseNumber() - - if err := dec.Decode(&versionInfo); err != nil { - return raw, versionInfo, fmt.Errorf("failed unmarshalling dashboard from path %s: %v", path, err) - } - return raw, versionInfo, nil + return DashboardMetaFromSchema(body), resp, nil } -func queryParams(params ...QueryParam) url.Values { - u := url.URL{} - q := u.Query() - - for _, p := range params { - p(&q) - } - return q +type DashboardCreateOpts struct { + Dashboard any + FolderID uint + FolderUID string + Message string + Overwrite bool } -func (c *Client) GetDashboardVersionsByDashboardUID(ctx context.Context, uid string, params ...QueryParam) ([]DashboardVersion, error) { - var ( - raw []byte - code int - err error - ) +func (o DashboardCreateOpts) Validate() error { + if o.FolderUID == "" && o.FolderID == 0 { + return errors.New("folder ID or UID missing") + } + if o.Dashboard == nil { + return errors.New("dashboard is nil") + } + return nil +} - if raw, code, err = c.get(ctx, fmt.Sprintf("api/dashboards/uid/%s/versions", uid), queryParams(params...)); err != nil { +type DashboardCreateResponse struct { + DashboardID uint + DashboardUID string + URL string + Status string + Version uint + Slug string +} + +func (c *DashboardClient) Create( + ctx context.Context, + opts DashboardCreateOpts, +) (*DashboardCreateResponse, *Response, error) { + if err := opts.Validate(); err != nil { + return nil, nil, err + } + + var reqBody schema.DashboardCreateRequest + reqBody.Dashboard = opts.Dashboard + reqBody.Overwrite = opts.Overwrite + reqBody.Message = opts.Message + if opts.FolderUID != "" { + reqBody.FolderUID = opts.FolderUID + } else { + reqBody.FolderID = opts.FolderID + } + + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/dashboards/db", bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.DashboardCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + + return DashboardCreateResponseFromSchema(respBody), resp, nil +} + +func (c *DashboardClient) Delete(ctx context.Context, uid string) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/dashboards/uid/%s", uid), nil) + if err != nil { return nil, err } - if code != 200 { - return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw) - } - var versions []DashboardVersion - err = json.Unmarshal(raw, &versions) - - return versions, err -} - -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 buf.String(), nil + return c.client.Do(req, nil) } diff --git a/pkg/grafana/dashboard_version.go b/pkg/grafana/dashboard_version.go new file mode 100644 index 0000000..cee05c7 --- /dev/null +++ b/pkg/grafana/dashboard_version.go @@ -0,0 +1,163 @@ +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 + } + + dashboards, resp, err := c.get(ctx, dashboardVersionURL, params...) + if err != nil || len(dashboards) < 1 { + return nil, resp, err + } + + return dashboards[0], resp, nil +} + +func (c *DashboardVersionClient) GetByUID( + ctx context.Context, + uid string, + version uint, + params ...DashboardVersionParam, +) (*DashboardVersion, *Response, error) { + dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/id/%s/versions/%d", uid, version)) + if err != nil { + return nil, nil, err + } + + dashboards, resp, err := c.get(ctx, dashboardVersionURL, params...) + if err != nil || len(dashboards) < 1 { + return nil, resp, err + } + + return dashboards[0], resp, nil +} + +func (c *DashboardVersionClient) Get( + ctx context.Context, + input string, + version uint, + params ...DashboardVersionParam, +) (*DashboardVersion, *Response, error) { + if id, err := strconv.Atoi(input); err == nil { + return c.GetByID(ctx, uint(id), version, params...) + } + return c.GetByUID(ctx, input, version, params...) +} + +func (c *DashboardVersionClient) ListByID( + ctx context.Context, + id uint, + params ...DashboardVersionParam, +) ([]*DashboardVersion, *Response, error) { + dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/id/%d/versions", id)) + if err != nil { + return nil, nil, err + } + + return c.get(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.get(ctx, dashboardVersionURL, params...) +} + +func (c *DashboardVersionClient) List( + ctx context.Context, + input string, + params ...DashboardVersionParam, +) ([]*DashboardVersion, *Response, error) { + if id, err := strconv.Atoi(input); err == nil { + return c.ListByID(ctx, uint(id), params...) + } + return c.ListByUID(ctx, input, params...) +} + +func (c *DashboardVersionClient) get( + ctx context.Context, + dashboardVersionURL *url.URL, + params ...DashboardVersionParam, +) ([]*DashboardVersion, *Response, error) { + if len(params) > 0 { + query := dashboardVersionURL.Query() + + for _, param := range params { + param(&query) + } + + dashboardVersionURL.RawQuery = query.Encode() + } + + req, err := c.client.NewRequest(ctx, "GET", dashboardVersionURL.String(), nil) + if err != nil { + return nil, nil, err + } + + var body schema.DashboardVersionListResponse + + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, resp, err + } + + dashboardVersions := make([]*DashboardVersion, 0, len(body.DashboardVersions)) + for _, dashboardVersion := range body.DashboardVersions { + dashboardVersions = append(dashboardVersions, DashboardVersionFromSchema(dashboardVersion)) + } + + return dashboardVersions, resp, nil +} diff --git a/pkg/grafana/requests.go b/pkg/grafana/requests.go deleted file mode 100644 index 176060d..0000000 --- a/pkg/grafana/requests.go +++ /dev/null @@ -1,82 +0,0 @@ -package grafana - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "path" - "strings" -) - -var DefaultHTTPClient = http.DefaultClient - -type Client struct { - baseURL string - key string - basicAuth bool - client *http.Client -} - -func NewClient(apiURL, authString string, client *http.Client) (*Client, error) { - baseURL, err := url.Parse(apiURL) - if err != nil { - return nil, err - } - - var key string - basicAuth := strings.Contains(authString, ":") - - 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 new file mode 100644 index 0000000..3c4891b --- /dev/null +++ b/pkg/grafana/schema.go @@ -0,0 +1,98 @@ +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 DashboardMetaFromSchema(source schema.DashboardMeta) *DashboardMeta { + return &DashboardMeta{ + IsStarred: source.IsStarred, + Type: source.Type, + CanSave: source.CanSave, + CanEdit: source.CanEdit, + CanAdmin: source.CanAdmin, + CanStar: source.CanStar, + CanDelete: source.CanDelete, + Slug: source.Slug, + URL: source.URL, + Expires: source.Expires, + Created: source.Created, + Updated: source.Updated, + UpdatedBy: source.UpdatedBy, + CreatedBy: source.CreatedBy, + Version: source.Version, + HasACL: source.HasACL, + IsFolder: source.IsFolder, + FolderID: source.FolderID, + FolderUID: source.FolderUID, + FolderTitle: source.FolderTitle, + FolderURL: source.FolderURL, + Provisioned: source.Provisioned, + ProvisionedExternalID: source.ProvisionedExternalID, + AnnotationsPermissions: AnnotationsPermissions{ + Dashboard: AnnotationPermissions{ + CanAdd: source.AnnotationsPermissions.Dashboard.CanAdd, + CanEdit: source.AnnotationsPermissions.Dashboard.CanEdit, + CanDelete: source.AnnotationsPermissions.Dashboard.CanDelete, + }, + Organization: AnnotationPermissions{ + CanAdd: source.AnnotationsPermissions.Organization.CanAdd, + CanEdit: source.AnnotationsPermissions.Organization.CanEdit, + CanDelete: source.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, + } +} diff --git a/pkg/grafana/schema/dashboard.go b/pkg/grafana/schema/dashboard.go new file mode 100644 index 0000000..c7afb1c --- /dev/null +++ b/pkg/grafana/schema/dashboard.go @@ -0,0 +1,59 @@ +package schema + +import "time" + +type DashboardCreateRequest struct { + Dashboard any `json:"dasboard"` + 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 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 struct { + Dashboard struct { + CanAdd bool `json:"canAdd"` + CanEdit bool `json:"canEdit"` + CanDelete bool `json:"canDelete"` + } `json:"dashboard"` + Organization struct { + CanAdd bool `json:"canAdd"` + CanEdit bool `json:"canEdit"` + CanDelete bool `json:"canDelete"` + } `json:"organization"` + } `json:"annotationsPermissions"` + Dashboard any `json:"dashboard"` +} diff --git a/pkg/grafana/schema/dashboard_version.go b/pkg/grafana/schema/dashboard_version.go new file mode 100644 index 0000000..6dc11cf --- /dev/null +++ b/pkg/grafana/schema/dashboard_version.go @@ -0,0 +1,20 @@ +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 struct { + DashboardVersions []DashboardVersion +} diff --git a/pkg/grafana/schema/error.go b/pkg/grafana/schema/error.go new file mode 100644 index 0000000..f3f06a0 --- /dev/null +++ b/pkg/grafana/schema/error.go @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..71fd4a3 --- /dev/null +++ b/pkg/grafana/schema/search.go @@ -0,0 +1,22 @@ +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 struct { + SearchResults []SearchResult +} diff --git a/pkg/grafana/search.go b/pkg/grafana/search.go index 7303a2a..3722a2b 100644 --- a/pkg/grafana/search.go +++ b/pkg/grafana/search.go @@ -2,68 +2,30 @@ package grafana import ( "context" - "encoding/json" - "fmt" "net/url" "strconv" + + "git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema" +) + +type ( + SearchParam func(*url.Values) + + SearchType string ) const ( - SearchTypeFolder SearchParamType = "dash-folder" - SearchTypeDashboard SearchParamType = "dash-db" + SearchTypeFolder SearchType = "dash-folder" + SearchTypeDashboard SearchType = "dash-db" ) -type FoundBoard struct { - ID uint `json:"id"` - UID string `json:"uid"` - Title string `json:"title"` - URI string `json:"uri"` - URL string `json:"url"` - Slug string `json:"slug"` - Type string `json:"type"` - Tags []string `json:"tags"` - IsStarred bool `json:"isStarred"` - FolderID int `json:"folderId"` - FolderUID string `json:"folderUid"` - FolderTitle string `json:"folderTitle"` - FolderURL string `json:"folderUrl"` +func WithType(searchType SearchType) SearchParam { + return func(v *url.Values) { + v.Set("type", string(searchType)) + } } -type ( - // SearchParam is a type for specifying Search params. - SearchParam func(*url.Values) - // SearchParamType is a type accepted by SearchType func. - SearchParamType string - // QueryParam is a type for specifying arbitrary API parameters - QueryParam func(*url.Values) -) - -func (c *Client) Search(ctx context.Context, params ...SearchParam) ([]FoundBoard, error) { - var ( - raw []byte - boards []FoundBoard - code int - err error - ) - u := url.URL{} - q := u.Query() - - for _, p := range params { - p(&q) - } - - if raw, code, err = c.get(ctx, "api/search", q); err != nil { - return nil, err - } - if code != 200 { - return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw) - } - - err = json.Unmarshal(raw, &boards) - return boards, err -} - -func SearchQuery(query string) SearchParam { +func WithQuery(query string) SearchParam { return func(v *url.Values) { if query != "" { v.Set("query", query) @@ -71,13 +33,13 @@ func SearchQuery(query string) SearchParam { } } -func SearchStarred(starred bool) SearchParam { +func WithStarred() SearchParam { return func(v *url.Values) { - v.Set("starred", strconv.FormatBool(starred)) + v.Set("starred", strconv.FormatBool(true)) } } -func SearchTag(tag string) SearchParam { +func WithTag(tag string) SearchParam { return func(v *url.Values) { if tag != "" { v.Add("tag", tag) @@ -85,8 +47,59 @@ func SearchTag(tag string) SearchParam { } } -func SearchType(searchType SearchParamType) SearchParam { - return func(v *url.Values) { - v.Set("type", string(searchType)) - } +type SearchResult struct { + ID uint + UID string + Title string + URI string + URL string + Slug string + Type string + Tags []string + IsStarred bool + SortMeta uint + FolderID int + FolderUID string + FolderTitle string + FolderURL string +} + +type SearchClient struct { + client *Client +} + +func (c *SearchClient) Search(ctx context.Context, params ...SearchParam) ([]*SearchResult, *Response, error) { + searchURL, err := url.Parse("/search") + if err != nil { + return nil, nil, err + } + + if len(params) > 0 { + query := searchURL.Query() + + for _, param := range params { + param(&query) + } + + searchURL.RawQuery = query.Encode() + } + + req, err := c.client.NewRequest(ctx, "GET", searchURL.String(), nil) + if err != nil { + return nil, nil, err + } + + var body schema.SearchResultListResponse + + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, resp, err + } + + searchResults := make([]*SearchResult, 0, len(body.SearchResults)) + for _, searchResult := range body.SearchResults { + searchResults = append(searchResults, SearchResultFromSchema(searchResult)) + } + + return searchResults, resp, nil } From 373c961debce2162a5bc402b451adfb1bf296a68 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sun, 23 Jun 2024 16:25:55 +0200 Subject: [PATCH 05/26] refactor(git): rework git package --- pkg/git/commit.go | 154 +++++++++++++++++++++++++++++ pkg/git/git.go | 241 --------------------------------------------- pkg/git/project.go | 79 +++++++++++++++ pkg/git/signer.go | 28 ------ 4 files changed, 233 insertions(+), 269 deletions(-) create mode 100644 pkg/git/commit.go delete mode 100644 pkg/git/git.go create mode 100644 pkg/git/project.go delete mode 100644 pkg/git/signer.go diff --git a/pkg/git/commit.go b/pkg/git/commit.go new file mode 100644 index 0000000..50793cd --- /dev/null +++ b/pkg/git/commit.go @@ -0,0 +1,154 @@ +package git + +import ( + "errors" + "os" + "time" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "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, file billy.File) CommitOption { + return func(c *Commit) { + c.Content = content + c.File = file + } +} + +type Commit struct { + Author *object.Signature + Committer *object.Signature + Content []byte + File billy.File + + project *Project +} + +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 { + var ( + signer *openpgp.Entity + ) + + if c.project.KeyFile != "" { + file, err := os.Open(c.project.KeyFile) + if err != nil { + return err + } + defer file.Close() + + block, err := armor.Decode(file) + if err != nil { + return err + } + + entityList, err := openpgp.ReadKeyRing(block.Body) + if err != nil || len(entityList) < 1 { + return err + } + + signer = entityList[0] + } + + worktree, err := c.project.repository.Worktree() + if err != nil { + return err + } + + _, err = c.project.repository.Branch(c.project.Branch) + if errors.Is(err, git.ErrBranchNotFound) { + err = c.project.repository.CreateBranch( + &config.Branch{Name: c.project.Branch, Remote: c.project.Branch}, + ) + if err != nil { + return err + } + } + if err != nil { + return err + } + + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: plumbing.ReferenceName(c.project.Branch), + }) + if err != nil { + return err + } + + if _, err = c.File.Write(c.Content); err != nil { + return err + } + + if err = c.File.Close(); err != nil { + return err + } + + if _, err = worktree.Add(c.File.Name()); err != nil { + return err + } + + commitOpts := git.CommitOptions{Author: c.Author} + + if c.Committer != nil { + commitOpts.Committer = c.Committer + } + + if signer != nil { + commitOpts.SignKey = signer + } + + _, err = worktree.Commit(msg, &commitOpts) + return err +} + +func (c *Commit) Push() error { + origin, err := c.project.repository.Remote("origin") + if err != nil { + return err + } + + pushOpts := git.PushOptions{} + + if c.project.auth != nil { + pushOpts.Auth = c.project.auth + } + + return origin.Push(&pushOpts) +} diff --git a/pkg/git/git.go b/pkg/git/git.go deleted file mode 100644 index b2b5df1..0000000 --- a/pkg/git/git.go +++ /dev/null @@ -1,241 +0,0 @@ -package git - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "git.ar21.de/yolokube/grafana-backuper/pkg/grafana" - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/storage/memory" -) - -var ( - fs billy.Filesystem - storer *memory.Storage -) - -type Payload struct { - Author *object.Signature - Committer *object.Signature - Content []byte - Dashboard grafana.FoundBoard - DashboardInfo grafana.BoardProperties - Directory string - KeyFile string - Repository *git.Repository - Version grafana.DashboardVersion -} - -func NewPayload(keyFile string) *Payload { - fs = memfs.New() - storer = memory.NewStorage() - - return &Payload{ - Author: nil, - Committer: nil, - Content: []byte{}, - Dashboard: grafana.FoundBoard{}, - DashboardInfo: grafana.BoardProperties{}, - Directory: "", - KeyFile: keyFile, - Repository: nil, - Version: grafana.DashboardVersion{}, - } -} - -func (p *Payload) AddAuthor(name, email string) { - p.Author = &object.Signature{ - Name: name, - Email: email, - When: time.Now(), - } -} - -func (p *Payload) UpdateAuthor(timestamp time.Time) { - p.Author.When = timestamp -} - -func (p *Payload) AddCommitter(name, email string) { - p.Committer = &object.Signature{ - Name: name, - Email: email, - When: time.Now(), - } -} - -func (p *Payload) UpdateCommitter() { - p.Committer.When = time.Now() -} - -func (p *Payload) UpdateContent(content []byte) { - p.Content = content -} - -func (p *Payload) UpdateDashboard(dashboard grafana.FoundBoard) { - p.Dashboard = dashboard -} - -func (p *Payload) UpdateDashboardInfo(dashboardInfo grafana.BoardProperties) { - p.DashboardInfo = dashboardInfo -} - -func (p *Payload) UpdateVersion(version grafana.DashboardVersion) { - p.Version = version -} - -func (p *Payload) GetRepo(repoURL, user, password string) (err error) { - p.Repository, err = git.Clone( - storer, - fs, - &git.CloneOptions{ - Auth: genAuth(user, password), - URL: repoURL, - Progress: os.Stdout, - }, - ) - - p.Directory = filepath.Base(repoURL) - - return -} - -func (p *Payload) IsVersionCommitted(branch string) bool { - refName := plumbing.NewBranchReferenceName(branch) - - ref, err := p.Repository.Reference(refName, false) - if err != nil { - return false - } - - commitIter, err := p.Repository.Log(&git.LogOptions{From: ref.Hash()}) - if err != nil { - return false - } - - err = commitIter.ForEach(func(commit *object.Commit) error { - if strings.Contains(commit.Message, fmt.Sprintf("Update %s", p.Version.DashboardUID)) && strings.Contains(commit.Message, fmt.Sprintf("version %d", p.Version.ID)) { - return fmt.Errorf("version already committed") - } - return nil - }) - - return err != nil -} - -func genAuth(user, password string) *http.BasicAuth { - return &http.BasicAuth{ - Username: user, - Password: password, - } -} - -func commitDashboard(repo *git.Repository, content []byte, commitMsg, dashboardTitle, folderTitle, gitRepoDirectory, keyFile string, author, committer *object.Signature) (err error) { - var ( - file billy.File - signer *openpgp.Entity - worktree *git.Worktree - ) - - if strings.TrimSpace(keyFile) != "" { - signer, err = getSigner(keyFile) - if err != nil { - return - } - } - - worktree, err = repo.Worktree() - if err != nil { - return - } - - if err = fs.MkdirAll(folderTitle, 0755); err != nil { - return - } - - filePath := filepath.Join(folderTitle, fmt.Sprintf("%s.json", dashboardTitle)) - if file, err = fs.Create(filePath); err != nil { - return - } - - if _, err = file.Write(content); err != nil { - return - } - - if err = file.Close(); err != nil { - return - } - - if _, err = worktree.Add(filePath); err != nil { - return - } - - _, err = worktree.Commit( - commitMsg, - &git.CommitOptions{ - Author: author, - Committer: committer, - SignKey: signer, - }, - ) - - return -} - -func (p *Payload) CreateCommit() error { - var commitmsg string - - if p.Version.Message != "" { - commitmsg = fmt.Sprintf( - "%s: Update %s to version %d => %s", - p.Dashboard.Title, - p.Version.DashboardUID, - p.Version.ID, - p.Version.Message, - ) - } else { - commitmsg = fmt.Sprintf( - "%s: Update %s to version %d", - p.Dashboard.Title, - p.Version.DashboardUID, - p.Version.ID, - ) - } - - p.UpdateAuthor(p.Version.Created) - p.UpdateCommitter() - - return commitDashboard( - p.Repository, - p.Content, - commitmsg, - p.Dashboard.Title, - p.DashboardInfo.FolderTitle, - p.Directory, - p.KeyFile, - p.Author, - p.Committer, - ) -} - -func (p *Payload) PushToRemote(user, password string) error { - origin, err := p.Repository.Remote("origin") - if err != nil { - return err - } - - return origin.Push( - &git.PushOptions{ - Auth: genAuth(user, password), - Progress: os.Stdout, - }, - ) -} diff --git a/pkg/git/project.go b/pkg/git/project.go new file mode 100644 index 0000000..ea823b2 --- /dev/null +++ b/pkg/git/project.go @@ -0,0 +1,79 @@ +package git + +import ( + "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/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 WithKey(path string) ProjectOption { + return func(p *Project) { + p.KeyFile = path + } +} + +type Project struct { + Branch string + RepoURL string + KeyFile string + + auth transport.AuthMethod + fs billy.Filesystem + storer *memory.Storage + repository *git.Repository +} + +func NewProject(url string, options ...ProjectOption) *Project { + project := &Project{ + Branch: "", + RepoURL: url, + KeyFile: "", + fs: memfs.New(), + storer: memory.NewStorage(), + repository: nil, + } + + for _, option := range options { + option(project) + } + + return project +} + +func (p *Project) Clone() error { + cloneOpts := git.CloneOptions{ + URL: p.RepoURL, + } + + if p.auth != nil { + cloneOpts.Auth = p.auth + } + + var err error + p.repository, err = git.Clone( + p.storer, + p.fs, + &cloneOpts, + ) + return err +} diff --git a/pkg/git/signer.go b/pkg/git/signer.go deleted file mode 100644 index 1834d06..0000000 --- a/pkg/git/signer.go +++ /dev/null @@ -1,28 +0,0 @@ -package git - -import ( - "os" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" -) - -func getSigner(keyFile string) (*openpgp.Entity, error) { - file, err := os.Open(keyFile) - if err != nil { - return nil, err - } - defer file.Close() - - block, err := armor.Decode(file) - if err != nil { - return nil, err - } - - entityList, err := openpgp.ReadKeyRing(block.Body) - if err != nil { - return nil, err - } - - return entityList[0], nil -} From cc49a79c8197003a766dd4790c8bddbff8f087e7 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 1 Jul 2024 00:54:05 +0200 Subject: [PATCH 06/26] feat(config): add config package --- go.mod | 1 + go.sum | 7 +++++++ internal/config/config.go | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 internal/config/config.go diff --git a/go.mod b/go.mod index f25a302..9ae5da0 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/alecthomas/kong v0.9.0 github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 + github.com/peterbourgon/ff v1.7.1 golang.org/x/net v0.22.0 ) diff --git a/go.sum b/go.sum index 8b762db..6714b3a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -55,8 +56,12 @@ 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/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk= +github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0= 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= @@ -140,6 +145,7 @@ 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 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 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= @@ -148,6 +154,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV 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.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..9da470a --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "flag" + "os" + + "github.com/peterbourgon/ff" +) + +const envVarPrefix = "GB" + +type Config struct { + ForceCommits bool + GrafanaURL string + GrafanaToken string + GitBranch string + GitRepo string + GitUser string + GitEmail string + GitPass string + GPGKey string +} + +func (c *Config) RegisterFlags(fs *flag.FlagSet) error { + fs.BoolVar(&c.ForceCommits, "force-commits", false, "Force git commits / ignore existence check") + fs.StringVar(&c.GrafanaURL, "grafana-url", "", "Grafana URL to access the API") + fs.StringVar(&c.GrafanaToken, "grafana-token", "", "Grafana auth token to access the API") + fs.StringVar(&c.GitBranch, "git-branch", "main", "Git branch name") + fs.StringVar(&c.GitRepo, "git-repo", "", "Complete Git repository URL") + fs.StringVar(&c.GitUser, "git-user", "", "Git username") + fs.StringVar(&c.GitEmail, "git-email", "", "Git email address") + fs.StringVar(&c.GitPass, "git-pass", "", "Git user password") + fs.StringVar(&c.GPGKey, "signing-key", "", "Path to the GPG signing key") + + return ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix(envVarPrefix)) +} From 55c59e0164ba66d9554a3483af09d86f2cb1e59a Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 1 Jul 2024 00:56:05 +0200 Subject: [PATCH 07/26] chore(renovate): adjust renovate configurations --- renovate.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 98cd62a..9dcb565 100644 --- a/renovate.json +++ b/renovate.json @@ -1,7 +1,8 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:base", + "docker:enableMajor" ], "packageRules": [ { @@ -11,5 +12,10 @@ "platformAutomerge": true, "dependencyDashboard": true } - ] + ], + "postUpdateOptions": [ + "gomodTidy", + "gomodUpdateImportPaths" + ], + "semanticCommits": "enabled" } \ No newline at end of file From 4aad919153e2d138912f7111b85b28a161d6b1be Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sat, 27 Jul 2024 18:52:47 +0200 Subject: [PATCH 08/26] refactor(git): split / improve clone function --- pkg/git/commit.go | 119 ++++++++++++++++-------------------------- pkg/git/project.go | 123 +++++++++++++++++++++++++++++++++++++++----- pkg/git/sign_key.go | 35 +++++++++++++ 3 files changed, 189 insertions(+), 88 deletions(-) create mode 100644 pkg/git/sign_key.go diff --git a/pkg/git/commit.go b/pkg/git/commit.go index 50793cd..49ed170 100644 --- a/pkg/git/commit.go +++ b/pkg/git/commit.go @@ -2,15 +2,13 @@ package git import ( "errors" - "os" + "fmt" + "path/filepath" + "strings" "time" "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/go-git/go-billy/v5" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) @@ -36,10 +34,16 @@ func WithCommitter(name, email string) CommitOption { } } -func WithFileContent(content []byte, file billy.File) CommitOption { +func WithFileContent(content []byte, filename, folder string) CommitOption { return func(c *Commit) { c.Content = content - c.File = file + c.Filename = filepath.Join(folder, fmt.Sprintf("%s.json", filename)) + } +} + +func WithSigner(signKey SignKey) CommitOption { + return func(c *Commit) { + c.signKey = signKey.entity } } @@ -47,9 +51,11 @@ type Commit struct { Author *object.Signature Committer *object.Signature Content []byte - File billy.File + Filename string + KeyFile string project *Project + signKey *openpgp.Entity } func (p *Project) NewCommit(options ...CommitOption) *Commit { @@ -63,64 +69,11 @@ func (p *Project) NewCommit(options ...CommitOption) *Commit { } func (c *Commit) Create(msg string) error { - var ( - signer *openpgp.Entity - ) - - if c.project.KeyFile != "" { - file, err := os.Open(c.project.KeyFile) - if err != nil { - return err - } - defer file.Close() - - block, err := armor.Decode(file) - if err != nil { - return err - } - - entityList, err := openpgp.ReadKeyRing(block.Body) - if err != nil || len(entityList) < 1 { - return err - } - - signer = entityList[0] - } - - worktree, err := c.project.repository.Worktree() - if err != nil { + if err := c.addContent(); err != nil { return err } - _, err = c.project.repository.Branch(c.project.Branch) - if errors.Is(err, git.ErrBranchNotFound) { - err = c.project.repository.CreateBranch( - &config.Branch{Name: c.project.Branch, Remote: c.project.Branch}, - ) - if err != nil { - return err - } - } - if err != nil { - return err - } - - err = worktree.Checkout(&git.CheckoutOptions{ - Branch: plumbing.ReferenceName(c.project.Branch), - }) - if err != nil { - return err - } - - if _, err = c.File.Write(c.Content); err != nil { - return err - } - - if err = c.File.Close(); err != nil { - return err - } - - if _, err = worktree.Add(c.File.Name()); err != nil { + if _, err := c.project.worktree.Add(c.Filename); err != nil { return err } @@ -130,25 +83,41 @@ func (c *Commit) Create(msg string) error { commitOpts.Committer = c.Committer } - if signer != nil { - commitOpts.SignKey = signer + if c.signKey != nil { + commitOpts.SignKey = c.signKey } - _, err = worktree.Commit(msg, &commitOpts) - return err -} - -func (c *Commit) Push() error { - origin, err := c.project.repository.Remote("origin") + _, err := c.project.worktree.Commit(msg, &commitOpts) if err != nil { return err } - pushOpts := git.PushOptions{} + return nil +} - if c.project.auth != nil { - pushOpts.Auth = c.project.auth +func (c *Commit) Exists(uid string, id uint) bool { + commitIter, err := c.project.repository.Log(&git.LogOptions{}) + if err != nil { + return false } - return origin.Push(&pushOpts) + err = commitIter.ForEach(func(commit *object.Commit) error { + if strings.Contains(commit.Message, fmt.Sprintf("Update %s", uid)) && + strings.Contains(commit.Message, fmt.Sprintf("version %d", id)) { + return errors.New("version already committed") + } + return nil + }) + return err != 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/project.go b/pkg/git/project.go index ea823b2..d10a145 100644 --- a/pkg/git/project.go +++ b/pkg/git/project.go @@ -1,9 +1,13 @@ package git import ( + "context" + "errors" + "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/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/storage/memory" @@ -26,28 +30,21 @@ func WithBranch(branch string) ProjectOption { } } -func WithKey(path string) ProjectOption { - return func(p *Project) { - p.KeyFile = path - } -} - type Project struct { Branch string + Force bool RepoURL string - KeyFile string auth transport.AuthMethod fs billy.Filesystem storer *memory.Storage repository *git.Repository + worktree *git.Worktree } func NewProject(url string, options ...ProjectOption) *Project { project := &Project{ - Branch: "", RepoURL: url, - KeyFile: "", fs: memfs.New(), storer: memory.NewStorage(), repository: nil, @@ -60,20 +57,120 @@ func NewProject(url string, options ...ProjectOption) *Project { return project } -func (p *Project) Clone() error { +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, + URL: p.RepoURL, + RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, } if p.auth != nil { cloneOpts.Auth = p.auth } + if err := cloneOpts.Validate(); err != nil { + return err + } + var err error - p.repository, err = git.Clone( + p.repository, err = git.CloneContext( + ctx, p.storer, p.fs, &cloneOpts, ) - return err + if err != nil { + return err + } + + return nil +} + +func (p *Project) HasChanges() bool { + remoteBranchRef := plumbing.NewRemoteReferenceName("origin", p.Branch) + remoteBranch, err := p.repository.Reference(remoteBranchRef, true) + if errors.Is(err, plumbing.ErrReferenceNotFound) { + return true + } else if err != nil { + return false + } + + localBranchRef, err := p.repository.Reference(plumbing.NewBranchReferenceName(p.Branch), true) + if err != nil { + return false + } + + if localBranchRef.Hash() != remoteBranch.Hash() { + return true + } + + return false +} + +func (p *Project) Pull(ctx context.Context) error { + pullOpts := git.PullOptions{ + ReferenceName: plumbing.NewBranchReferenceName(p.Branch), + } + + if err := pullOpts.Validate(); err != nil { + return err + } + + err := p.worktree.PullContext(ctx, &pullOpts) + if !errors.Is(err, plumbing.ErrReferenceNotFound) && 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 err := pushOpts.Validate(); err != nil { + return err + } + + return p.repository.PushContext(ctx, &pushOpts) } diff --git a/pkg/git/sign_key.go b/pkg/git/sign_key.go new file mode 100644 index 0000000..5ac350b --- /dev/null +++ b/pkg/git/sign_key.go @@ -0,0 +1,35 @@ +package git + +import ( + "os" + + "github.com/ProtonMail/go-crypto/openpgp" + "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) + if err != nil { + return err + } + defer file.Close() + + block, err := armor.Decode(file) + if err != nil { + return err + } + + entityList, err := openpgp.ReadKeyRing(block.Body) + if err != nil || len(entityList) < 1 { + return err + } + + s.entity = entityList[0] + return nil +} From 577f821b9d23820eac8c8e494972a2d40f36f7d5 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sat, 27 Jul 2024 18:53:38 +0200 Subject: [PATCH 09/26] refactor(grafana): replace dashboard info with import compatible layout --- pkg/grafana/client.go | 6 +- pkg/grafana/dashboard.go | 20 ++--- pkg/grafana/dashboard_version.go | 58 ++++++++----- pkg/grafana/schema.go | 105 +++++++++++++++++------- pkg/grafana/schema/dashboard.go | 78 +++++++++--------- pkg/grafana/schema/dashboard_version.go | 4 +- pkg/grafana/schema/search.go | 4 +- pkg/grafana/search.go | 8 +- 8 files changed, 173 insertions(+), 110 deletions(-) diff --git a/pkg/grafana/client.go b/pkg/grafana/client.go index 3f973e8..ee4ab86 100644 --- a/pkg/grafana/client.go +++ b/pkg/grafana/client.go @@ -67,8 +67,8 @@ func NewClient(endpoint string, options ...ClientOption) *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) +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 @@ -102,7 +102,7 @@ type ErrorResponse struct { TraceID uint } -func (c *Client) Do(req *http.Request, v any) (*Response, error) { +func (c *Client) do(req *http.Request, v any) (*Response, error) { httpResp, err := c.httpClient.Do(req) resp := &Response{Response: httpResp} if err != nil { diff --git a/pkg/grafana/dashboard.go b/pkg/grafana/dashboard.go index 2fa27a5..08a850d 100644 --- a/pkg/grafana/dashboard.go +++ b/pkg/grafana/dashboard.go @@ -11,7 +11,7 @@ import ( "git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema" ) -type DashboardMeta struct { +type Dashboard struct { IsStarred bool Type string CanSave bool @@ -54,20 +54,20 @@ type DashboardClient struct { client *Client } -func (c *DashboardClient) Get(ctx context.Context, uid string) (*DashboardMeta, *Response, error) { - req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/dashboards/uid/%s", uid), nil) +func (c *DashboardClient) Get(ctx context.Context, uid string) (*Dashboard, *Response, error) { + req, err := c.client.newRequest(ctx, "GET", fmt.Sprintf("/dashboards/uid/%s", uid), nil) if err != nil { return nil, nil, err } - var body schema.DashboardMeta + var body schema.Dashboard - resp, err := c.client.Do(req, &body) + resp, err := c.client.do(req, &body) if err != nil { return nil, resp, err } - return DashboardMetaFromSchema(body), resp, nil + return DashboardFromSchema(body), resp, nil } type DashboardCreateOpts struct { @@ -120,13 +120,13 @@ func (c *DashboardClient) Create( return nil, nil, err } - req, err := c.client.NewRequest(ctx, "POST", "/dashboards/db", bytes.NewReader(reqBodyData)) + 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) + resp, err := c.client.do(req, &respBody) if err != nil { return nil, resp, err } @@ -135,10 +135,10 @@ func (c *DashboardClient) Create( } 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) + req, err := c.client.newRequest(ctx, "DELETE", fmt.Sprintf("/dashboards/uid/%s", uid), nil) if err != nil { return nil, err } - return c.client.Do(req, nil) + return c.client.do(req, nil) } diff --git a/pkg/grafana/dashboard_version.go b/pkg/grafana/dashboard_version.go index cee05c7..fa0404f 100644 --- a/pkg/grafana/dashboard_version.go +++ b/pkg/grafana/dashboard_version.go @@ -51,12 +51,7 @@ func (c *DashboardVersionClient) GetByID( return nil, nil, err } - dashboards, resp, err := c.get(ctx, dashboardVersionURL, params...) - if err != nil || len(dashboards) < 1 { - return nil, resp, err - } - - return dashboards[0], resp, nil + return c.get(ctx, dashboardVersionURL, params...) } func (c *DashboardVersionClient) GetByUID( @@ -65,17 +60,12 @@ func (c *DashboardVersionClient) GetByUID( version uint, params ...DashboardVersionParam, ) (*DashboardVersion, *Response, error) { - dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/id/%s/versions/%d", uid, version)) + dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/uid/%s/versions/%d", uid, version)) if err != nil { return nil, nil, err } - dashboards, resp, err := c.get(ctx, dashboardVersionURL, params...) - if err != nil || len(dashboards) < 1 { - return nil, resp, err - } - - return dashboards[0], resp, nil + return c.get(ctx, dashboardVersionURL, params...) } func (c *DashboardVersionClient) Get( @@ -100,7 +90,7 @@ func (c *DashboardVersionClient) ListByID( return nil, nil, err } - return c.get(ctx, dashboardVersionURL, params...) + return c.list(ctx, dashboardVersionURL, params...) } func (c *DashboardVersionClient) ListByUID( @@ -113,7 +103,7 @@ func (c *DashboardVersionClient) ListByUID( return nil, nil, err } - return c.get(ctx, dashboardVersionURL, params...) + return c.list(ctx, dashboardVersionURL, params...) } func (c *DashboardVersionClient) List( @@ -131,6 +121,36 @@ 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() @@ -142,20 +162,20 @@ func (c *DashboardVersionClient) get( dashboardVersionURL.RawQuery = query.Encode() } - req, err := c.client.NewRequest(ctx, "GET", dashboardVersionURL.String(), nil) + 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) + resp, err := c.client.do(req, &body) if err != nil { return nil, resp, err } - dashboardVersions := make([]*DashboardVersion, 0, len(body.DashboardVersions)) - for _, dashboardVersion := range body.DashboardVersions { + dashboardVersions := make([]*DashboardVersion, 0, len(body)) + for _, dashboardVersion := range body { dashboardVersions = append(dashboardVersions, DashboardVersionFromSchema(dashboardVersion)) } diff --git a/pkg/grafana/schema.go b/pkg/grafana/schema.go index 3c4891b..afed52c 100644 --- a/pkg/grafana/schema.go +++ b/pkg/grafana/schema.go @@ -15,41 +15,41 @@ func DashboardCreateResponseFromSchema(source schema.DashboardCreateResponse) *D } } -func DashboardMetaFromSchema(source schema.DashboardMeta) *DashboardMeta { - return &DashboardMeta{ - IsStarred: source.IsStarred, - Type: source.Type, - CanSave: source.CanSave, - CanEdit: source.CanEdit, - CanAdmin: source.CanAdmin, - CanStar: source.CanStar, - CanDelete: source.CanDelete, - Slug: source.Slug, - URL: source.URL, - Expires: source.Expires, - Created: source.Created, - Updated: source.Updated, - UpdatedBy: source.UpdatedBy, - CreatedBy: source.CreatedBy, - Version: source.Version, - HasACL: source.HasACL, - IsFolder: source.IsFolder, - FolderID: source.FolderID, - FolderUID: source.FolderUID, - FolderTitle: source.FolderTitle, - FolderURL: source.FolderURL, - Provisioned: source.Provisioned, - ProvisionedExternalID: source.ProvisionedExternalID, +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.AnnotationsPermissions.Dashboard.CanAdd, - CanEdit: source.AnnotationsPermissions.Dashboard.CanEdit, - CanDelete: source.AnnotationsPermissions.Dashboard.CanDelete, + CanAdd: source.Meta.AnnotationsPermissions.Dashboard.CanAdd, + CanEdit: source.Meta.AnnotationsPermissions.Dashboard.CanEdit, + CanDelete: source.Meta.AnnotationsPermissions.Dashboard.CanDelete, }, Organization: AnnotationPermissions{ - CanAdd: source.AnnotationsPermissions.Organization.CanAdd, - CanEdit: source.AnnotationsPermissions.Organization.CanEdit, - CanDelete: source.AnnotationsPermissions.Organization.CanDelete, + CanAdd: source.Meta.AnnotationsPermissions.Organization.CanAdd, + CanEdit: source.Meta.AnnotationsPermissions.Organization.CanEdit, + CanDelete: source.Meta.AnnotationsPermissions.Organization.CanDelete, }, }, Dashboard: source.Dashboard, @@ -96,3 +96,46 @@ func SearchResultFromSchema(source schema.SearchResult) *SearchResult { 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 index c7afb1c..c381608 100644 --- a/pkg/grafana/schema/dashboard.go +++ b/pkg/grafana/schema/dashboard.go @@ -19,41 +19,45 @@ type DashboardCreateResponse struct { Slug string `json:"slug"` } -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 struct { - Dashboard struct { - CanAdd bool `json:"canAdd"` - CanEdit bool `json:"canEdit"` - CanDelete bool `json:"canDelete"` - } `json:"dashboard"` - Organization struct { - CanAdd bool `json:"canAdd"` - CanEdit bool `json:"canEdit"` - CanDelete bool `json:"canDelete"` - } `json:"organization"` - } `json:"annotationsPermissions"` - Dashboard any `json:"dashboard"` +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 index 6dc11cf..45f2643 100644 --- a/pkg/grafana/schema/dashboard_version.go +++ b/pkg/grafana/schema/dashboard_version.go @@ -15,6 +15,4 @@ type DashboardVersion struct { Data any `json:"data"` } -type DashboardVersionListResponse struct { - DashboardVersions []DashboardVersion -} +type DashboardVersionListResponse []DashboardVersion diff --git a/pkg/grafana/schema/search.go b/pkg/grafana/schema/search.go index 71fd4a3..eef07e2 100644 --- a/pkg/grafana/schema/search.go +++ b/pkg/grafana/schema/search.go @@ -17,6 +17,4 @@ type SearchResult struct { FolderURL string `json:"folderUrl"` } -type SearchResultListResponse struct { - SearchResults []SearchResult -} +type SearchResultListResponse []SearchResult diff --git a/pkg/grafana/search.go b/pkg/grafana/search.go index 3722a2b..16912c2 100644 --- a/pkg/grafana/search.go +++ b/pkg/grafana/search.go @@ -84,20 +84,20 @@ func (c *SearchClient) Search(ctx context.Context, params ...SearchParam) ([]*Se searchURL.RawQuery = query.Encode() } - req, err := c.client.NewRequest(ctx, "GET", searchURL.String(), nil) + 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) + resp, err := c.client.do(req, &body) if err != nil { return nil, resp, err } - searchResults := make([]*SearchResult, 0, len(body.SearchResults)) - for _, searchResult := range body.SearchResults { + searchResults := make([]*SearchResult, 0, len(body)) + for _, searchResult := range body { searchResults = append(searchResults, SearchResultFromSchema(searchResult)) } From 62b316b5efac630298ad48dce62ae2e0d96b9f70 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sat, 27 Jul 2024 18:56:09 +0200 Subject: [PATCH 10/26] refactor(cmd): add backup command to cli --- cfg/cfg.go | 99 ------------------------ go.mod | 22 +++++- go.sum | 53 +++++++++++++ internal/cmd/backup.go | 148 ++++++++++++++++++++++++++++++++++++ internal/cmd/root.go | 76 ++++++++++++++++++ internal/config/config.go | 52 ++++++++----- internal/version/version.go | 17 +++++ main.go | 83 ++------------------ 8 files changed, 357 insertions(+), 193 deletions(-) delete mode 100644 cfg/cfg.go create mode 100644 internal/cmd/backup.go create mode 100644 internal/cmd/root.go create mode 100644 internal/version/version.go diff --git a/cfg/cfg.go b/cfg/cfg.go deleted file mode 100644 index dac8eb7..0000000 --- a/cfg/cfg.go +++ /dev/null @@ -1,99 +0,0 @@ -package cfg - -import ( - "fmt" - "os" - - "github.com/alecthomas/kong" -) - -type AppSettings struct { - Force bool - GrafanaURL string - GrafanaToken string - GitBranch string - GitRepoURL string - GitUser string - GitEmail string - GitPass string - GPGKey string -} - -var cliStruct struct { - Force bool `name:"force" short:"f" help:"Force git commits / ignore existence check."` - GrafanaURL string `name:"grafana-url" env:"GRAFANA_URL" help:"Grafana URL to access the API"` - GrafanaToken string `name:"grafana-auth-token" env:"GRAFANA_AUTH_TOKEN" help:"Grafana auth token to access the API"` - GitBranch string `name:"git-branch" env:"GIT_BRANCH" help:"Git branch name" default:"${default_git_branch}"` - GitRepoURL string `name:"git-repo-url" env:"GIT_REPO_URL" help:"Complete Git repository URL"` - GitUser string `name:"git-user" env:"GIT_USER" help:"Git username"` - GitEmail string `name:"git-email" env:"GIT_EMAIL" help:"Git email address"` - GitPass string `name:"git-pass" env:"GIT_PASS" help:"Git user password"` - GPGKey string `name:"signing-key" env:"GIT_SIGNING_KEY" help:"GPG signing key"` -} - -func Parse() *AppSettings { - ctx := kong.Parse( - &cliStruct, - kong.Vars{ - "default_git_branch": "main", - }, - kong.Name("grafana-backuper"), - kong.Description("🚀 CLI tool to convert grafana dashboards to git"), - kong.UsageOnError(), - ) - - validateFlags(ctx) - return &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, - } -} - -func validateFlags(cliCtx *kong.Context) { - flagsValid := true - 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 { - if err := cliCtx.PrintUsage(false); err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - fmt.Println() - for i := 0; i < len(messages); i++ { - fmt.Println(messages[i]) - } - os.Exit(1) - } -} diff --git a/go.mod b/go.mod index 9ae5da0..6521940 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,10 @@ require ( github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/peterbourgon/ff v1.7.1 - golang.org/x/net v0.22.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 + golang.org/x/net v0.23.0 ) require ( @@ -17,18 +20,35 @@ require ( github.com/cloudflare/circl v1.3.7 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // 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/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.15.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.18.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6714b3a..fdef2d5 100644 --- a/go.sum +++ b/go.sum @@ -20,15 +20,19 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/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/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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -43,8 +47,12 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l 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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -56,10 +64,17 @@ 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/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +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/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk= github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -68,21 +83,52 @@ 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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +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.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/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/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 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= @@ -90,6 +136,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 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/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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= @@ -103,6 +151,8 @@ 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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -151,10 +201,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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.2.4/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 new file mode 100644 index 0000000..9b18f82 --- /dev/null +++ b/internal/cmd/backup.go @@ -0,0 +1,148 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "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" + "github.com/spf13/cobra" +) + +func NewBackupCommand(c *config.Config) *cobra.Command { + backupCmd := &cobra.Command{ + Use: "backup", + Short: "Back up the dashboards from grafana to a git repository.", + Long: "Back up the dashboards from grafana to a git repository.", + RunE: func(cmd *cobra.Command, _ []string) error { + return backup(cmd.Context(), c) + }, + PreRunE: func(_ *cobra.Command, _ []string) error { + return c.Validate() + }, + } + + backupCmd.PersistentFlags().BoolVar(&c.ForceCommits, "force", false, "Force git commits / ignore existence check") + backupCmd.PersistentFlags().StringVar(&c.GitEmail, "git-email", "", "Git email address") + backupCmd.PersistentFlags().StringVar(&c.GPGKey, "signing-key", "", "Path to the GPG signing key") + + return backupCmd +} + +func backup(ctx context.Context, c *config.Config) error { + client := grafana.NewClient( + c.GrafanaURL, + grafana.WithToken(c.GrafanaToken), + ) + + project := git.NewProject( + c.GitRepo, + git.WithBasicAuth(c.GitUser, c.GitPass), + git.WithBranch(c.GitBranch), + ) + + if err := project.Clone(ctx); err != nil { + return err + } + + if err := project.Checkout(); err != nil { + return err + } + + if err := project.Pull(ctx); err != nil { + return err + } + + var signer git.SignKey + if c.GPGKey != "" { + signer = git.SignKey{KeyFile: c.GPGKey} + if err := signer.ReadKeyFile(); err != nil { + return err + } + } + + dashboards, _, err := client.File.Search(ctx, grafana.WithType(grafana.SearchTypeDashboard)) + if err != nil { + return err + } + + for _, dashboardInfo := range dashboards { + var dashboard *grafana.Dashboard + dashboard, _, err = client.Dashboard.Get(ctx, dashboardInfo.UID) + if err != nil { + return err + } + + var versions []*grafana.DashboardVersion + versions, _, err = client.DashboardVersion.List(ctx, dashboardInfo.UID) + if err != nil { + return err + } + + slices.Reverse(versions) + + var uncommitedVersion bool + for _, version := range versions { + var dashboardVersion *grafana.DashboardVersion + dashboardVersion, _, err = client.DashboardVersion.Get(ctx, dashboardInfo.UID, version.Version) + if err != nil { + return err + } + + dashboard.Dashboard = dashboardVersion.Data + dashboard.UpdatedBy = dashboardVersion.CreatedBy + dashboard.Updated = dashboardVersion.Created + dashboard.Version = dashboardVersion.Version + + var data []byte + data, err = json.MarshalIndent(grafana.SchemaFromDashboardMeta(dashboard), "", " ") + if err != nil { + return err + } + + commitMsg := fmt.Sprintf( + "%s: Update %s to version %d", + dashboardInfo.Title, + dashboardInfo.UID, + dashboardVersion.ID, + ) + if dashboardVersion.Message != "" { + commitMsg += fmt.Sprintf(" => %s", dashboardVersion.Message) + } + + commitOpts := []git.CommitOption{ + git.WithAuthor(dashboardVersion.CreatedBy, ""), + git.WithFileContent(data, dashboardInfo.Title, dashboard.FolderTitle), + git.WithSigner(signer), + } + + if c.GitUser != "" { + commitOpts = append(commitOpts, git.WithCommitter(c.GitUser, c.GitEmail)) + } + + commit := project.NewCommit(commitOpts...) + + if commit.Exists(dashboardVersion.DashboardUID, dashboardVersion.ID) && !c.ForceCommits && !uncommitedVersion { + fmt.Fprintf(c.Output, "%s -> already committed\n", commitMsg) + continue + } + + uncommitedVersion = true + if err = commit.Create(commitMsg); err != nil { + return err + } + fmt.Fprintln(c.Output, commitMsg) + } + } + + if project.HasChanges() { + if err = project.Push(ctx); err != nil { + return err + } + } + + return nil +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..43bf4d8 --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "git.ar21.de/yolokube/grafana-backuper/internal/config" + "git.ar21.de/yolokube/grafana-backuper/internal/version" + "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(), + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + initializeConfig(cmd) + + c.Output = os.Stdout + if c.Quiet { + var err error + c.Output, err = os.Open(os.DevNull) + if err != nil { + return err + } + } + + cmd.SetOut(c.Output) + return nil + }, + SilenceUsage: true, + } + + rootCmd.SetErrPrefix("Error:") + + rootCmd.PersistentFlags().BoolVarP(&c.Debug, "debug", "d", false, "Debug output") + 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().BoolVarP(&c.Quiet, "quiet", "q", false, "Mute console output") + + 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 index 9da470a..c86a1ac 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,36 +1,52 @@ package config import ( - "flag" - "os" - - "github.com/peterbourgon/ff" + "errors" + "io" ) -const envVarPrefix = "GB" - type Config struct { + Debug bool ForceCommits bool GrafanaURL string GrafanaToken string GitBranch string + GitEmail string GitRepo string GitUser string - GitEmail string GitPass string GPGKey string + Quiet bool + + Output io.Writer } -func (c *Config) RegisterFlags(fs *flag.FlagSet) error { - fs.BoolVar(&c.ForceCommits, "force-commits", false, "Force git commits / ignore existence check") - fs.StringVar(&c.GrafanaURL, "grafana-url", "", "Grafana URL to access the API") - fs.StringVar(&c.GrafanaToken, "grafana-token", "", "Grafana auth token to access the API") - fs.StringVar(&c.GitBranch, "git-branch", "main", "Git branch name") - fs.StringVar(&c.GitRepo, "git-repo", "", "Complete Git repository URL") - fs.StringVar(&c.GitUser, "git-user", "", "Git username") - fs.StringVar(&c.GitEmail, "git-email", "", "Git email address") - fs.StringVar(&c.GitPass, "git-pass", "", "Git user password") - fs.StringVar(&c.GPGKey, "signing-key", "", "Path to the GPG signing key") +func (c *Config) Validate() error { + var err error - return ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix(envVarPrefix)) + if c.GrafanaURL == "" { + err = errors.Join(err, errors.New("invalid grafana URL, must not be blank")) + } + + if c.GrafanaToken == "" { + err = errors.Join(err, errors.New("invalid auth token for grafana, must not be blank")) + } + + if c.GitRepo == "" { + err = errors.Join(err, errors.New("invalid repo url for git, must not be blank")) + } + + if c.GitUser == "" { + err = errors.Join(err, errors.New("invalid username for git, must not be blank")) + } + + if c.GitPass == "" { + err = errors.Join(err, errors.New("invalid password for git, must not be blank")) + } + + if c.GitBranch == "" { + err = errors.Join(err, errors.New("invalid branch name for git, must not be blank")) + } + + return err } diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..bd65be1 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,17 @@ +package version + +import ( + "fmt" +) + +var ( + version = "undefined" + versionPrerelease = "dev" //nolint:gochecknoglobals // this has to be a variable to set the version during release. +) + +func Version() string { + if versionPrerelease != "" { + return fmt.Sprintf("%s-%s", version, versionPrerelease) + } + return version +} diff --git a/main.go b/main.go index fd8c1bf..bd7579b 100644 --- a/main.go +++ b/main.go @@ -1,86 +1,19 @@ package main import ( - "context" - "fmt" - "log" - "slices" + "os" - "git.ar21.de/yolokube/grafana-backuper/cfg" - "git.ar21.de/yolokube/grafana-backuper/pkg/git" - "git.ar21.de/yolokube/grafana-backuper/pkg/grafana" + "git.ar21.de/yolokube/grafana-backuper/internal/cmd" + "git.ar21.de/yolokube/grafana-backuper/internal/config" ) func main() { - appSettings := cfg.Parse() + cfg := &config.Config{} - 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) - } + rootCmd := cmd.NewRootCommand(cfg) + rootCmd.AddCommand(cmd.NewBackupCommand(cfg)) - 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) - } - } + if err := rootCmd.Execute(); err != nil { + os.Exit(1) } } From d46a9f0883374d897725fbb603751a5f71617d7c Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sat, 27 Jul 2024 19:16:55 +0200 Subject: [PATCH 11/26] refactor(git): add output option to git functions --- internal/cmd/backup.go | 1 + pkg/git/project.go | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index 9b18f82..f7afcca 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -42,6 +42,7 @@ func backup(ctx context.Context, c *config.Config) error { c.GitRepo, git.WithBasicAuth(c.GitUser, c.GitPass), git.WithBranch(c.GitBranch), + git.WithOutputWriter(c.Output), ) if err := project.Clone(ctx); err != nil { diff --git a/pkg/git/project.go b/pkg/git/project.go index d10a145..058f3df 100644 --- a/pkg/git/project.go +++ b/pkg/git/project.go @@ -3,6 +3,7 @@ package git import ( "context" "errors" + "io" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" @@ -30,6 +31,12 @@ func WithBranch(branch string) ProjectOption { } } +func WithOutputWriter(o io.Writer) ProjectOption { + return func(p *Project) { + p.writer = o + } +} + type Project struct { Branch string Force bool @@ -40,6 +47,7 @@ type Project struct { storer *memory.Storage repository *git.Repository worktree *git.Worktree + writer io.Writer } func NewProject(url string, options ...ProjectOption) *Project { @@ -103,6 +111,10 @@ func (p *Project) Clone(ctx context.Context) error { cloneOpts.Auth = p.auth } + if p.writer != nil { + cloneOpts.Progress = p.writer + } + if err := cloneOpts.Validate(); err != nil { return err } @@ -147,6 +159,10 @@ func (p *Project) Pull(ctx context.Context) error { ReferenceName: plumbing.NewBranchReferenceName(p.Branch), } + if p.writer != nil { + pullOpts.Progress = p.writer + } + if err := pullOpts.Validate(); err != nil { return err } @@ -168,6 +184,10 @@ func (p *Project) Push(ctx context.Context) error { pushOpts.Auth = p.auth } + if p.writer != nil { + pushOpts.Progress = p.writer + } + if err := pushOpts.Validate(); err != nil { return err } From 1d949f1fb8597ad860a9247e551f2a2cea070d12 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Tue, 30 Jul 2024 20:41:16 +0200 Subject: [PATCH 12/26] refactor(git): move commit exists function to project --- pkg/git/commit.go | 18 ------------------ pkg/git/project.go | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/pkg/git/commit.go b/pkg/git/commit.go index 49ed170..a4508c1 100644 --- a/pkg/git/commit.go +++ b/pkg/git/commit.go @@ -1,10 +1,8 @@ package git import ( - "errors" "fmt" "path/filepath" - "strings" "time" "github.com/ProtonMail/go-crypto/openpgp" @@ -95,22 +93,6 @@ func (c *Commit) Create(msg string) error { return nil } -func (c *Commit) Exists(uid string, id uint) bool { - commitIter, err := c.project.repository.Log(&git.LogOptions{}) - if err != nil { - return false - } - - err = commitIter.ForEach(func(commit *object.Commit) error { - if strings.Contains(commit.Message, fmt.Sprintf("Update %s", uid)) && - strings.Contains(commit.Message, fmt.Sprintf("version %d", id)) { - return errors.New("version already committed") - } - return nil - }) - return err != nil -} - func (c *Commit) addContent() error { file, err := c.project.worktree.Filesystem.Create(c.Filename) if err != nil { diff --git a/pkg/git/project.go b/pkg/git/project.go index 058f3df..512cf79 100644 --- a/pkg/git/project.go +++ b/pkg/git/project.go @@ -3,12 +3,15 @@ package git import ( "context" "errors" + "fmt" "io" + "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" @@ -133,6 +136,22 @@ func (p *Project) Clone(ctx context.Context) error { return nil } +func (p *Project) CommitExists(uid string, id uint) bool { + commitIter, err := p.repository.Log(&git.LogOptions{}) + if err != nil { + return false + } + + err = commitIter.ForEach(func(commit *object.Commit) error { + if strings.Contains(commit.Message, fmt.Sprintf("Update %s", uid)) && + strings.Contains(commit.Message, fmt.Sprintf("version %d", id)) { + return errors.New("version already committed") + } + return nil + }) + return err != nil +} + func (p *Project) HasChanges() bool { remoteBranchRef := plumbing.NewRemoteReferenceName("origin", p.Branch) remoteBranch, err := p.repository.Reference(remoteBranchRef, true) From 0c05b9ee2c7972279a7e97a5159338e87decb37e Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Tue, 30 Jul 2024 20:43:58 +0200 Subject: [PATCH 13/26] chore(cmd/backup): split up backup function to helper package --- internal/cmd/backup.go | 83 ++++++++++-------------------------- internal/helper/helper.go | 90 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 61 deletions(-) create mode 100644 internal/helper/helper.go diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index f7afcca..dbb545e 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -2,11 +2,12 @@ package cmd import ( "context" - "encoding/json" + "errors" "fmt" "slices" "git.ar21.de/yolokube/grafana-backuper/internal/config" + "git.ar21.de/yolokube/grafana-backuper/internal/helper" "git.ar21.de/yolokube/grafana-backuper/pkg/git" "git.ar21.de/yolokube/grafana-backuper/pkg/grafana" "github.com/spf13/cobra" @@ -38,29 +39,15 @@ func backup(ctx context.Context, c *config.Config) error { grafana.WithToken(c.GrafanaToken), ) - project := git.NewProject( - c.GitRepo, - git.WithBasicAuth(c.GitUser, c.GitPass), - git.WithBranch(c.GitBranch), - git.WithOutputWriter(c.Output), - ) - - if err := project.Clone(ctx); err != nil { - return err - } - - if err := project.Checkout(); err != nil { - return err - } - - if err := project.Pull(ctx); err != nil { + project, err := helper.PrepareProject(ctx, c) + if err != nil { return err } var signer git.SignKey if c.GPGKey != "" { signer = git.SignKey{KeyFile: c.GPGKey} - if err := signer.ReadKeyFile(); err != nil { + if err = signer.ReadKeyFile(); err != nil { return err } } @@ -85,7 +72,7 @@ func backup(ctx context.Context, c *config.Config) error { slices.Reverse(versions) - var uncommitedVersion bool + uncommitedVersion := c.ForceCommits for _, version := range versions { var dashboardVersion *grafana.DashboardVersion dashboardVersion, _, err = client.DashboardVersion.Get(ctx, dashboardInfo.UID, version.Version) @@ -93,57 +80,31 @@ func backup(ctx context.Context, c *config.Config) error { return err } - dashboard.Dashboard = dashboardVersion.Data - dashboard.UpdatedBy = dashboardVersion.CreatedBy - dashboard.Updated = dashboardVersion.Created - dashboard.Version = dashboardVersion.Version - - var data []byte - data, err = json.MarshalIndent(grafana.SchemaFromDashboardMeta(dashboard), "", " ") - if err != nil { - return err - } - - commitMsg := fmt.Sprintf( - "%s: Update %s to version %d", - dashboardInfo.Title, - dashboardInfo.UID, - dashboardVersion.ID, + var commitMsg string + commitMsg, err = helper.CommitVersion( + dashboard, + dashboardVersion, + dashboardInfo, + project, + c, + signer, + uncommitedVersion, ) - if dashboardVersion.Message != "" { - commitMsg += fmt.Sprintf(" => %s", dashboardVersion.Message) - } - - commitOpts := []git.CommitOption{ - git.WithAuthor(dashboardVersion.CreatedBy, ""), - git.WithFileContent(data, dashboardInfo.Title, dashboard.FolderTitle), - git.WithSigner(signer), - } - - if c.GitUser != "" { - commitOpts = append(commitOpts, git.WithCommitter(c.GitUser, c.GitEmail)) - } - - commit := project.NewCommit(commitOpts...) - - if commit.Exists(dashboardVersion.DashboardUID, dashboardVersion.ID) && !c.ForceCommits && !uncommitedVersion { - fmt.Fprintf(c.Output, "%s -> already committed\n", commitMsg) + if errors.Is(err, helper.ErrAlreadyCommited) { + fmt.Fprintf(c.Output, "%s -> %v\n", commitMsg, err) continue + } else if err != nil { + return err } uncommitedVersion = true - if err = commit.Create(commitMsg); err != nil { - return err - } fmt.Fprintln(c.Output, commitMsg) } } - if project.HasChanges() { - if err = project.Push(ctx); err != nil { - return err - } + if !project.HasChanges() { + return nil } - return nil + return project.Push(ctx) } diff --git a/internal/helper/helper.go b/internal/helper/helper.go new file mode 100644 index 0000000..37add25 --- /dev/null +++ b/internal/helper/helper.go @@ -0,0 +1,90 @@ +package helper + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "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") + +func CreateCommitMsg(d *grafana.DashboardVersion, title, uid string) string { + commitMsg := fmt.Sprintf("%s: Update %s to version %d", title, uid, d.ID) + + if d.Message != "" { + commitMsg += fmt.Sprintf(" => %s", d.Message) + } + + return commitMsg +} + +func CommitVersion( + dashboard *grafana.Dashboard, + dashboardVersion *grafana.DashboardVersion, + dashboardInfo *grafana.SearchResult, + project *git.Project, + cfg *config.Config, + signer git.SignKey, + force bool, +) (string, error) { + commitMsg := CreateCommitMsg(dashboardVersion, dashboardInfo.Title, dashboardInfo.UID) + + if !force && project.CommitExists(dashboardVersion.DashboardUID, dashboardVersion.ID) { + return commitMsg, ErrAlreadyCommited + } + + UpdateDashboardInfo(dashboard, dashboardVersion) + + 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), + git.WithSigner(signer), + } + + if cfg.GitUser != "" { + commitOpts = append(commitOpts, git.WithCommitter(cfg.GitUser, cfg.GitEmail)) + } + + commit := project.NewCommit(commitOpts...) + return commitMsg, commit.Create(commitMsg) +} + +func PrepareProject(ctx context.Context, c *config.Config) (*git.Project, error) { + project := git.NewProject( + c.GitRepo, + git.WithBasicAuth(c.GitUser, c.GitPass), + git.WithBranch(c.GitBranch), + git.WithOutputWriter(c.Output), + ) + + if err := project.Clone(ctx); err != nil { + return nil, err + } + + if err := project.Checkout(); err != nil { + return nil, err + } + + if err := project.Pull(ctx); err != nil { + return nil, err + } + + return project, nil +} + +func UpdateDashboardInfo(d *grafana.Dashboard, dv *grafana.DashboardVersion) { + d.Dashboard = dv.Data + d.UpdatedBy = dv.CreatedBy + d.Updated = dv.Created + d.Version = dv.Version +} From 0c093bb5c9302df937a91b26495e3078801857da Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Tue, 30 Jul 2024 21:20:18 +0200 Subject: [PATCH 14/26] feat(backup): use logrus for output --- go.mod | 1 + go.sum | 3 +++ internal/cmd/backup.go | 18 +++++++++++------- internal/cmd/root.go | 18 ++++++++---------- internal/config/config.go | 1 - 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 6521940..ccfac3e 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/sagikazarmark/locafero v0.4.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/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect diff --git a/go.sum b/go.sum index fdef2d5..a0ac07a 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -115,6 +117,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index dbb545e..03a8196 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -3,13 +3,13 @@ package cmd import ( "context" "errors" - "fmt" "slices" "git.ar21.de/yolokube/grafana-backuper/internal/config" "git.ar21.de/yolokube/grafana-backuper/internal/helper" "git.ar21.de/yolokube/grafana-backuper/pkg/git" "git.ar21.de/yolokube/grafana-backuper/pkg/grafana" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -18,11 +18,15 @@ func NewBackupCommand(c *config.Config) *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.", - RunE: func(cmd *cobra.Command, _ []string) error { - return backup(cmd.Context(), c) + Run: func(cmd *cobra.Command, _ []string) { + if err := backup(cmd.Context(), c); err != nil { + log.WithContext(cmd.Context()).WithError(err).Fatal() + } }, - PreRunE: func(_ *cobra.Command, _ []string) error { - return c.Validate() + PreRun: func(cmd *cobra.Command, _ []string) { + if err := c.Validate(); err != nil { + log.WithContext(cmd.Context()).WithError(err).Fatal("checking flags & environment variables") + } }, } @@ -91,14 +95,14 @@ func backup(ctx context.Context, c *config.Config) error { uncommitedVersion, ) if errors.Is(err, helper.ErrAlreadyCommited) { - fmt.Fprintf(c.Output, "%s -> %v\n", commitMsg, err) + log.WithContext(ctx).WithField("commit-msg", commitMsg).Info("already committed") continue } else if err != nil { return err } uncommitedVersion = true - fmt.Fprintln(c.Output, commitMsg) + log.WithContext(ctx).WithField("commit-msg", commitMsg).Info("commit created") } } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 43bf4d8..30fb913 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -7,6 +7,7 @@ import ( "git.ar21.de/yolokube/grafana-backuper/internal/config" "git.ar21.de/yolokube/grafana-backuper/internal/version" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -20,20 +21,18 @@ func NewRootCommand(c *config.Config) *cobra.Command { Short: "Grafana Backuper CLI", Long: "A command-line tool to back up and restore Grafana dashboards", Version: version.Version(), - PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + PersistentPreRun: func(cmd *cobra.Command, _ []string) { initializeConfig(cmd) c.Output = os.Stdout - if c.Quiet { - var err error - c.Output, err = os.Open(os.DevNull) - if err != nil { - return err - } - } + log.SetOutput(c.Output) + log.SetLevel(log.InfoLevel) cmd.SetOut(c.Output) - return nil + + if c.Debug { + log.SetLevel(log.DebugLevel) + } }, SilenceUsage: true, } @@ -47,7 +46,6 @@ func NewRootCommand(c *config.Config) *cobra.Command { 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().BoolVarP(&c.Quiet, "quiet", "q", false, "Mute console output") return rootCmd } diff --git a/internal/config/config.go b/internal/config/config.go index c86a1ac..cc6ac4e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,7 +16,6 @@ type Config struct { GitUser string GitPass string GPGKey string - Quiet bool Output io.Writer } From 681342848985b0b45a035de2550560e816b9a491 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Tue, 30 Jul 2024 21:20:54 +0200 Subject: [PATCH 15/26] chore(cmd): move main file to cmd directory --- main.go => cmd/main.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename main.go => cmd/main.go (100%) diff --git a/main.go b/cmd/main.go similarity index 100% rename from main.go rename to cmd/main.go From 74cb5e02643391237afad8907406830b5a71ce8b Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Tue, 30 Jul 2024 21:21:34 +0200 Subject: [PATCH 16/26] refactor(Dockerfile): use new backup command as default --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a157fdf..c0df2a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update --yes && \ apt-get install --yes build-essential # Build the actual binary -RUN CGO_ENABLED=0 go build -o grafana-backuper main.go +RUN CGO_ENABLED=0 go build -o grafana-backuper cmd/main.go # -- -- -- -- -- -- @@ -22,4 +22,4 @@ WORKDIR /app # Copy built binary from build image COPY --from=build /workspace/grafana-backuper /app -ENTRYPOINT ["/app/grafana-backuper"] +ENTRYPOINT ["/app/grafana-backuper backup"] From 453041b4b23ec216436d133434b25932a630228c Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 14:08:51 +0200 Subject: [PATCH 17/26] refactor(grafanabackuper): add grafanabackuper package with backup functionality --- pkg/grafanabackuper/backup.go | 159 +++++++++++++++++++++++++++++++++ pkg/grafanabackuper/client.go | 84 +++++++++++++++++ pkg/grafanabackuper/message.go | 20 +++++ 3 files changed, 263 insertions(+) create mode 100644 pkg/grafanabackuper/backup.go create mode 100644 pkg/grafanabackuper/client.go create mode 100644 pkg/grafanabackuper/message.go diff --git a/pkg/grafanabackuper/backup.go b/pkg/grafanabackuper/backup.go new file mode 100644 index 0000000..67fc63b --- /dev/null +++ b/pkg/grafanabackuper/backup.go @@ -0,0 +1,159 @@ +package grafanabackuper + +import ( + "context" + "encoding/json" + "errors" + "slices" + + "git.ar21.de/yolokube/grafana-backuper/internal/config" + "git.ar21.de/yolokube/grafana-backuper/pkg/git" + "git.ar21.de/yolokube/grafana-backuper/pkg/grafana" +) + +var ErrAlreadyCommited = errors.New("already committed") + +type BackupClient struct { + client *Client +} + +func (b *BackupClient) Start(ctx context.Context, cfg *config.Config) error { + b.client.logger.Debug().Str("search-type", string(grafana.SearchTypeDashboard)).Msg("Searching dashboards by type") + dashboards, _, err := b.client.grafana.File.Search(ctx, grafana.WithType(grafana.SearchTypeDashboard)) + if err != nil { + return err + } + b.client.logger.Debug(). + Str("search-type", string(grafana.SearchTypeDashboard)). + Int("counter", len(dashboards)). + Msg("Found dashboards") + + for _, dashboardInfo := range dashboards { + b.client.logger.Debug(). + Str("dashboard-uid", dashboardInfo.UID). + Str("dashboard", dashboardInfo.Title). + Msg("Fetching dashboard data") + var dashboard *grafana.Dashboard + dashboard, _, err = b.client.grafana.Dashboard.Get(ctx, dashboardInfo.UID) + if err != nil { + return err + } + + b.client.logger.Debug(). + Str("dashboard-uid", dashboardInfo.UID). + Str("dashboard", dashboardInfo.Title). + Msg("Fetching dashboard versions") + var versions []*grafana.DashboardVersion + versions, _, err = b.client.grafana.DashboardVersion.List(ctx, dashboardInfo.UID) + if err != nil { + return err + } + b.client.logger.Debug(). + Str("dashboard-uid", dashboardInfo.UID). + Str("dashboard", dashboardInfo.Title). + Int("counter", len(versions)). + Msg("Found dashboard versions") + + slices.Reverse(versions) + + uncommitedVersion := cfg.ForceCommits + for _, version := range versions { + b.client.logger.Debug(). + Str("dashboard-uid", dashboardInfo.UID). + Uint("version", version.Version). + Msg("Fetching version data") + var dashboardVersion *grafana.DashboardVersion + dashboardVersion, _, err = b.client.grafana.DashboardVersion.Get(ctx, dashboardInfo.UID, version.Version) + if err != nil { + return err + } + + var commitmsg string + commitmsg, err = b.commitDashboardVersion( + dashboard, + dashboardVersion, + dashboardInfo, + cfg, + uncommitedVersion, + ) + if errors.Is(err, ErrAlreadyCommited) { + b.client.logger.Info().Str("commit-msg", commitmsg).Msg("Already committed") + continue + } else if err != nil { + return err + } + + uncommitedVersion = true + b.client.logger.Info().Str("commit-msg", commitmsg).Msg("Commit created") + } + } + + if !b.client.git.HasChanges() { + return nil + } + + return b.client.git.Push(ctx) +} + +func (b *BackupClient) commitDashboardVersion( + dashboard *grafana.Dashboard, + dashboardVersion *grafana.DashboardVersion, + dashboardInfo *grafana.SearchResult, + cfg *config.Config, + force bool, +) (string, error) { + commitmsg := Message{ + ID: dashboardVersion.ID, + Path: dashboardInfo.Title, + Title: dashboardVersion.Message, + UID: dashboardInfo.UID, + }.String() + + b.client.logger.Debug().Str("commit", commitmsg).Msg("Checking commit existence") + if !force && b.client.git.CommitExists(commitmsg) { + return commitmsg, ErrAlreadyCommited + } + + b.updateDashboardInfo(dashboard, dashboardVersion) + + b.client.logger.Debug(). + Str("folder", dashboard.FolderTitle). + Str("dashboard-uid", dashboardVersion.DashboardUID). + Msg("Marshalling the dashboard version data") + data, err := json.MarshalIndent(grafana.SchemaFromDashboardMeta(dashboard), "", " ") + if err != nil { + return commitmsg, err + } + + commitOpts := []git.CommitOption{ + git.WithAuthor(dashboardVersion.CreatedBy, ""), + git.WithFileContent(data, dashboardInfo.Title, dashboard.FolderTitle), + } + + if b.client.signer != nil { + commitOpts = append(commitOpts, git.WithSigner(*b.client.signer)) + } + + if cfg.GitUser != "" { + commitOpts = append(commitOpts, git.WithCommitter(cfg.GitUser, cfg.GitEmail)) + } + + commit := b.client.git.NewCommit(commitOpts...) + b.client.logger.Debug().Str("commit", commitmsg).Msg("Creating new commit") + return commitmsg, commit.Create(commitmsg) +} + +func (b *BackupClient) updateDashboardInfo(d *grafana.Dashboard, dv *grafana.DashboardVersion) { + b.client.logger.Debug(). + Str("dashboard-uid", dv.DashboardUID). + Str("folder", d.FolderTitle). + Msg("Updating dashboard information") + d.Dashboard = dv.Data + d.UpdatedBy = dv.CreatedBy + d.Updated = dv.Created + d.Version = dv.Version + b.client.logger.Debug(). + Str("dashboard-uid", dv.DashboardUID). + Str("folder", d.FolderTitle). + Msg("Updated dashboard information successfully") +} diff --git a/pkg/grafanabackuper/client.go b/pkg/grafanabackuper/client.go new file mode 100644 index 0000000..23cf7e3 --- /dev/null +++ b/pkg/grafanabackuper/client.go @@ -0,0 +1,84 @@ +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 +} + +func NewClient(options ...ClientOption) *Client { + client := &Client{} + + for _, option := range options { + option(client) + } + + client.Backup = BackupClient{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 new file mode 100644 index 0000000..e239107 --- /dev/null +++ b/pkg/grafanabackuper/message.go @@ -0,0 +1,20 @@ +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 +} From 638bb23b903403cbb7922c27df21d1f4b6e7b660 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 14:10:46 +0200 Subject: [PATCH 18/26] style(git): preload commit logs & adjust changes check --- pkg/git/project.go | 61 +++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/pkg/git/project.go b/pkg/git/project.go index 512cf79..789deb8 100644 --- a/pkg/git/project.go +++ b/pkg/git/project.go @@ -3,9 +3,7 @@ package git import ( "context" "errors" - "fmt" "io" - "strings" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" @@ -41,9 +39,10 @@ func WithOutputWriter(o io.Writer) ProjectOption { } type Project struct { - Branch string - Force bool - RepoURL string + Branch string + Force bool + RepoURL string + CommitLogs map[string]*object.Commit auth transport.AuthMethod fs billy.Filesystem @@ -56,6 +55,7 @@ type Project struct { 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, @@ -136,41 +136,40 @@ func (p *Project) Clone(ctx context.Context) error { return nil } -func (p *Project) CommitExists(uid string, id uint) bool { +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 } - err = commitIter.ForEach(func(commit *object.Commit) error { - if strings.Contains(commit.Message, fmt.Sprintf("Update %s", uid)) && - strings.Contains(commit.Message, fmt.Sprintf("version %d", id)) { - return errors.New("version already committed") - } - return nil - }) - return err != nil -} - -func (p *Project) HasChanges() bool { - remoteBranchRef := plumbing.NewRemoteReferenceName("origin", p.Branch) - remoteBranch, err := p.repository.Reference(remoteBranchRef, true) + 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 } - localBranchRef, err := p.repository.Reference(plumbing.NewBranchReferenceName(p.Branch), true) - if err != nil { - return false - } - - if localBranchRef.Hash() != remoteBranch.Hash() { - return true - } - - return false + return localBranchRef.Hash() != remoteBranchRef.Hash() } func (p *Project) Pull(ctx context.Context) error { @@ -187,7 +186,9 @@ func (p *Project) Pull(ctx context.Context) error { } err := p.worktree.PullContext(ctx, &pullOpts) - if !errors.Is(err, plumbing.ErrReferenceNotFound) && err != nil { + if !errors.Is(err, plumbing.ErrReferenceNotFound) && + !errors.Is(err, git.NoErrAlreadyUpToDate) && + err != nil { return err } From 840008df2d11bc76e2e5e156dcb13bd16b25e3d9 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 14:12:00 +0200 Subject: [PATCH 19/26] feat(internal/logger): add logger package with different style presets --- internal/logger/logger.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 internal/logger/logger.go diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..fbcc944 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,28 @@ +package logger + +import ( + "fmt" + "io" + "path/filepath" + "time" + + "github.com/rs/zerolog" +) + +func CliLoggerLayout(out io.Writer) zerolog.Logger { + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + return fmt.Sprintf("%s:%d", filepath.Base(file), line) + } + + if zerolog.GlobalLevel() == zerolog.DebugLevel { + debugOutput := zerolog.ConsoleWriter{Out: out, TimeFormat: time.RFC3339} + return zerolog.New(debugOutput).With().Timestamp().Caller().Logger() + } + + output := zerolog.ConsoleWriter{Out: out, PartsExclude: []string{"time", "level"}} + return zerolog.New(output).With().Logger() +} + +func JSONLoggerLayout(out io.Writer) zerolog.Logger { + return zerolog.New(out).With().Timestamp().Caller().Logger() +} From 83a15b5a87a4e1831c3a5ce197a9c51ceb89cfb8 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 14:13:16 +0200 Subject: [PATCH 20/26] refactor(internal/cmd): move backend cmd functions to grafanabackuper package --- Dockerfile | 2 +- go.mod | 37 ++++++-------- go.sum | 105 ++++++++++++++++---------------------- internal/cmd/backup.go | 95 ++++++++-------------------------- internal/cmd/root.go | 25 +++++---- internal/config/config.go | 22 ++++---- internal/helper/helper.go | 90 -------------------------------- 7 files changed, 112 insertions(+), 264 deletions(-) delete mode 100644 internal/helper/helper.go diff --git a/Dockerfile b/Dockerfile index c0df2a6..d25a8ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,4 +22,4 @@ WORKDIR /app # Copy built binary from build image COPY --from=build /workspace/grafana-backuper /app -ENTRYPOINT ["/app/grafana-backuper backup"] +ENTRYPOINT ["/app/grafana-backuper backup --json"] diff --git a/go.mod b/go.mod index ccfac3e..44497ab 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,23 @@ module git.ar21.de/yolokube/grafana-backuper -go 1.22.2 +go 1.22.5 require ( github.com/ProtonMail/go-crypto v1.0.0 - github.com/alecthomas/kong v0.9.0 github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 - github.com/peterbourgon/ff v1.7.1 + github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 - golang.org/x/net v0.23.0 + golang.org/x/net v0.28.0 ) require ( - dario.cat/mergo v1.0.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cloudflare/circl v1.3.9 // indirect + github.com/cyphar/filepath-securejoin v0.3.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -28,27 +27,25 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/mod v0.15.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.18.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a0ac07a..6af7d4b 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,32 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= -github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -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.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/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.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= +github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/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= @@ -43,14 +39,13 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj 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.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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -66,44 +61,46 @@ 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/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= -github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 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/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk= -github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0= 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.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +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= @@ -115,9 +112,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -128,23 +123,19 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW 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/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -152,15 +143,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -170,19 +157,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -190,15 +179,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 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= @@ -209,7 +195,6 @@ 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.2.4/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= diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index 03a8196..e06c9fe 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -2,14 +2,11 @@ package cmd import ( "context" - "errors" - "slices" + "os" "git.ar21.de/yolokube/grafana-backuper/internal/config" - "git.ar21.de/yolokube/grafana-backuper/internal/helper" - "git.ar21.de/yolokube/grafana-backuper/pkg/git" - "git.ar21.de/yolokube/grafana-backuper/pkg/grafana" - log "github.com/sirupsen/logrus" + "git.ar21.de/yolokube/grafana-backuper/pkg/grafanabackuper" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -20,12 +17,20 @@ func NewBackupCommand(c *config.Config) *cobra.Command { 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.WithContext(cmd.Context()).WithError(err).Fatal() + log.Fatal().Err(err).Send() } }, PreRun: func(cmd *cobra.Command, _ []string) { - if err := c.Validate(); err != nil { - log.WithContext(cmd.Context()).WithError(err).Fatal("checking flags & environment variables") + 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) } }, } @@ -37,78 +42,18 @@ func NewBackupCommand(c *config.Config) *cobra.Command { return backupCmd } -func backup(ctx context.Context, c *config.Config) error { - client := grafana.NewClient( - c.GrafanaURL, - grafana.WithToken(c.GrafanaToken), +func backup(ctx context.Context, cfg *config.Config) error { + client := grafanabackuper.NewClient( + grafanabackuper.WithZerologLogger(cfg.Logger), ) - project, err := helper.PrepareProject(ctx, c) - if err != nil { + if err := client.GetSigner(cfg); err != nil { return err } - var signer git.SignKey - if c.GPGKey != "" { - signer = git.SignKey{KeyFile: c.GPGKey} - if err = signer.ReadKeyFile(); err != nil { - return err - } - } - - dashboards, _, err := client.File.Search(ctx, grafana.WithType(grafana.SearchTypeDashboard)) - if err != nil { + if err := client.Prepare(ctx, cfg); err != nil { return err } - for _, dashboardInfo := range dashboards { - var dashboard *grafana.Dashboard - dashboard, _, err = client.Dashboard.Get(ctx, dashboardInfo.UID) - if err != nil { - return err - } - - var versions []*grafana.DashboardVersion - versions, _, err = client.DashboardVersion.List(ctx, dashboardInfo.UID) - if err != nil { - return err - } - - slices.Reverse(versions) - - uncommitedVersion := c.ForceCommits - for _, version := range versions { - var dashboardVersion *grafana.DashboardVersion - dashboardVersion, _, err = client.DashboardVersion.Get(ctx, dashboardInfo.UID, version.Version) - if err != nil { - return err - } - - var commitMsg string - commitMsg, err = helper.CommitVersion( - dashboard, - dashboardVersion, - dashboardInfo, - project, - c, - signer, - uncommitedVersion, - ) - if errors.Is(err, helper.ErrAlreadyCommited) { - log.WithContext(ctx).WithField("commit-msg", commitMsg).Info("already committed") - continue - } else if err != nil { - return err - } - - uncommitedVersion = true - log.WithContext(ctx).WithField("commit-msg", commitMsg).Info("commit created") - } - } - - if !project.HasChanges() { - return nil - } - - return project.Push(ctx) + return client.Backup.Start(ctx, cfg) } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 30fb913..d2b06ea 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -6,8 +6,10 @@ import ( "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" - log "github.com/sirupsen/logrus" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -24,22 +26,27 @@ func NewRootCommand(c *config.Config) *cobra.Command { PersistentPreRun: func(cmd *cobra.Command, _ []string) { initializeConfig(cmd) - c.Output = os.Stdout - - log.SetOutput(c.Output) - log.SetLevel(log.InfoLevel) - cmd.SetOut(c.Output) + // c.Output = os.Stdout + cmd.SetOut(os.Stdout) + zerolog.SetGlobalLevel(zerolog.InfoLevel) if c.Debug { - log.SetLevel(log.DebugLevel) + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } + + 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.SetErrPrefix("Error:") - rootCmd.PersistentFlags().BoolVarP(&c.Debug, "debug", "d", false, "Debug output") + rootCmd.PersistentFlags().BoolVar(&c.JSONFormat, "json", false, "JSON output") 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") diff --git a/internal/config/config.go b/internal/config/config.go index cc6ac4e..1b1ef53 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,11 +3,14 @@ package config import ( "errors" "io" + + "github.com/rs/zerolog" ) type Config struct { Debug bool ForceCommits bool + JSONFormat bool GrafanaURL string GrafanaToken string GitBranch string @@ -18,34 +21,35 @@ type Config struct { GPGKey string Output io.Writer + Logger zerolog.Logger } -func (c *Config) Validate() error { - var err error +func (c *Config) Validate() []error { + var errs []error if c.GrafanaURL == "" { - err = errors.Join(err, errors.New("invalid grafana URL, must not be blank")) + errs = append(errs, errors.New("invalid grafana URL, must not be blank")) } if c.GrafanaToken == "" { - err = errors.Join(err, errors.New("invalid auth token for grafana, must not be blank")) + errs = append(errs, errors.New("invalid auth token for grafana, must not be blank")) } if c.GitRepo == "" { - err = errors.Join(err, errors.New("invalid repo url for git, must not be blank")) + errs = append(errs, errors.New("invalid repo url for git, must not be blank")) } if c.GitUser == "" { - err = errors.Join(err, errors.New("invalid username for git, must not be blank")) + errs = append(errs, errors.New("invalid username for git, must not be blank")) } if c.GitPass == "" { - err = errors.Join(err, errors.New("invalid password for git, must not be blank")) + errs = append(errs, errors.New("invalid password for git, must not be blank")) } if c.GitBranch == "" { - err = errors.Join(err, errors.New("invalid branch name for git, must not be blank")) + errs = append(errs, errors.New("invalid branch name for git, must not be blank")) } - return err + return errs } diff --git a/internal/helper/helper.go b/internal/helper/helper.go deleted file mode 100644 index 37add25..0000000 --- a/internal/helper/helper.go +++ /dev/null @@ -1,90 +0,0 @@ -package helper - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - "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") - -func CreateCommitMsg(d *grafana.DashboardVersion, title, uid string) string { - commitMsg := fmt.Sprintf("%s: Update %s to version %d", title, uid, d.ID) - - if d.Message != "" { - commitMsg += fmt.Sprintf(" => %s", d.Message) - } - - return commitMsg -} - -func CommitVersion( - dashboard *grafana.Dashboard, - dashboardVersion *grafana.DashboardVersion, - dashboardInfo *grafana.SearchResult, - project *git.Project, - cfg *config.Config, - signer git.SignKey, - force bool, -) (string, error) { - commitMsg := CreateCommitMsg(dashboardVersion, dashboardInfo.Title, dashboardInfo.UID) - - if !force && project.CommitExists(dashboardVersion.DashboardUID, dashboardVersion.ID) { - return commitMsg, ErrAlreadyCommited - } - - UpdateDashboardInfo(dashboard, dashboardVersion) - - 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), - git.WithSigner(signer), - } - - if cfg.GitUser != "" { - commitOpts = append(commitOpts, git.WithCommitter(cfg.GitUser, cfg.GitEmail)) - } - - commit := project.NewCommit(commitOpts...) - return commitMsg, commit.Create(commitMsg) -} - -func PrepareProject(ctx context.Context, c *config.Config) (*git.Project, error) { - project := git.NewProject( - c.GitRepo, - git.WithBasicAuth(c.GitUser, c.GitPass), - git.WithBranch(c.GitBranch), - git.WithOutputWriter(c.Output), - ) - - if err := project.Clone(ctx); err != nil { - return nil, err - } - - if err := project.Checkout(); err != nil { - return nil, err - } - - if err := project.Pull(ctx); err != nil { - return nil, err - } - - return project, nil -} - -func UpdateDashboardInfo(d *grafana.Dashboard, dv *grafana.DashboardVersion) { - d.Dashboard = dv.Data - d.UpdatedBy = dv.CreatedBy - d.Updated = dv.Created - d.Version = dv.Version -} From 1f0d76ba9edf6d39a19584d78038d87966df6bce Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 20:12:28 +0200 Subject: [PATCH 21/26] refactor(grafanabackuper): add restore function --- cmd/main.go | 5 +- internal/cmd/backup.go | 1 + internal/cmd/restore.go | 54 +++++++++++++++++ pkg/git/project.go | 35 +++++++++++ pkg/grafana/dashboard.go | 14 +++-- pkg/grafana/schema/dashboard.go | 2 +- pkg/grafanabackuper/client.go | 4 +- pkg/grafanabackuper/message.go | 4 +- pkg/grafanabackuper/restore.go | 103 ++++++++++++++++++++++++++++++++ 9 files changed, 214 insertions(+), 8 deletions(-) create mode 100644 internal/cmd/restore.go create mode 100644 pkg/grafanabackuper/restore.go diff --git a/cmd/main.go b/cmd/main.go index bd7579b..33d3200 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,7 +11,10 @@ func main() { cfg := &config.Config{} rootCmd := cmd.NewRootCommand(cfg) - rootCmd.AddCommand(cmd.NewBackupCommand(cfg)) + rootCmd.AddCommand( + cmd.NewBackupCommand(cfg), + cmd.NewRestoreCommand(cfg), + ) if err := rootCmd.Execute(); err != nil { os.Exit(1) diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index e06c9fe..fc7629c 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -10,6 +10,7 @@ import ( "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", diff --git a/internal/cmd/restore.go b/internal/cmd/restore.go new file mode 100644 index 0000000..247f798 --- /dev/null +++ b/internal/cmd/restore.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "context" + "os" + + "git.ar21.de/yolokube/grafana-backuper/internal/config" + "git.ar21.de/yolokube/grafana-backuper/pkg/grafanabackuper" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +//nolint:dupl // This function may be the same as or similar to other cmd functions. +func NewRestoreCommand(c *config.Config) *cobra.Command { + restoreCmd := &cobra.Command{ + Use: "restore", + Short: "Restore the dashboards from a git repository to grafana.", + Long: "Restore the dashboards from a git repository to grafana.", + Run: func(cmd *cobra.Command, _ []string) { + if err := restore(cmd.Context(), c); err != nil { + log.Fatal().Err(err).Send() + } + }, + PreRun: func(cmd *cobra.Command, _ []string) { + errs := c.Validate() + for _, err := range errs { + log.Error().Err(err).Send() + } + + if len(errs) > 0 { + if err := cmd.Help(); err != nil { + log.Error().Err(err).Send() + } + os.Exit(1) + } + }, + } + + restoreCmd.PersistentFlags().BoolVar(&c.ForceCommits, "force", false, "Force dashboards / ignore existence check") + + return restoreCmd +} + +func restore(ctx context.Context, cfg *config.Config) error { + client := grafanabackuper.NewClient( + grafanabackuper.WithZerologLogger(cfg.Logger), + ) + + if err := client.Prepare(ctx, cfg); err != nil { + return err + } + + return client.Restore.Start(ctx, cfg) +} diff --git a/pkg/git/project.go b/pkg/git/project.go index 789deb8..21d9bb7 100644 --- a/pkg/git/project.go +++ b/pkg/git/project.go @@ -4,6 +4,8 @@ import ( "context" "errors" "io" + "path/filepath" + "strings" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" @@ -214,3 +216,36 @@ func (p *Project) Push(ctx context.Context) error { 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/grafana/dashboard.go b/pkg/grafana/dashboard.go index 08a850d..666be85 100644 --- a/pkg/grafana/dashboard.go +++ b/pkg/grafana/dashboard.go @@ -79,9 +79,6 @@ type DashboardCreateOpts struct { } func (o DashboardCreateOpts) Validate() error { - if o.FolderUID == "" && o.FolderID == 0 { - return errors.New("folder ID or UID missing") - } if o.Dashboard == nil { return errors.New("dashboard is nil") } @@ -111,7 +108,7 @@ func (c *DashboardClient) Create( reqBody.Message = opts.Message if opts.FolderUID != "" { reqBody.FolderUID = opts.FolderUID - } else { + } else if opts.FolderID > 0 { reqBody.FolderID = opts.FolderID } @@ -142,3 +139,12 @@ func (c *DashboardClient) Delete(ctx context.Context, uid string) (*Response, er return c.client.do(req, nil) } + +func (c *DashboardClient) ParseRaw(raw []byte) (*Dashboard, error) { + var body schema.Dashboard + if err := json.Unmarshal(raw, &body); err != nil { + return nil, err + } + + return DashboardFromSchema(body), nil +} diff --git a/pkg/grafana/schema/dashboard.go b/pkg/grafana/schema/dashboard.go index c381608..c6207fb 100644 --- a/pkg/grafana/schema/dashboard.go +++ b/pkg/grafana/schema/dashboard.go @@ -3,7 +3,7 @@ package schema import "time" type DashboardCreateRequest struct { - Dashboard any `json:"dasboard"` + Dashboard any `json:"dashboard"` FolderID uint `json:"folderId,omitempty"` FolderUID string `json:"folderUid"` Message string `json:"message,omitempty"` diff --git a/pkg/grafanabackuper/client.go b/pkg/grafanabackuper/client.go index 23cf7e3..5fda218 100644 --- a/pkg/grafanabackuper/client.go +++ b/pkg/grafanabackuper/client.go @@ -23,7 +23,8 @@ type Client struct { logger zerolog.Logger signer *git.SignKey - Backup BackupClient + Backup BackupClient + Restore RestoreClient } func NewClient(options ...ClientOption) *Client { @@ -34,6 +35,7 @@ func NewClient(options ...ClientOption) *Client { } client.Backup = BackupClient{client: client} + client.Restore = RestoreClient{client: client} return client } diff --git a/pkg/grafanabackuper/message.go b/pkg/grafanabackuper/message.go index e239107..66a364b 100644 --- a/pkg/grafanabackuper/message.go +++ b/pkg/grafanabackuper/message.go @@ -1,6 +1,8 @@ package grafanabackuper -import "fmt" +import ( + "fmt" +) type Message struct { ID uint diff --git a/pkg/grafanabackuper/restore.go b/pkg/grafanabackuper/restore.go new file mode 100644 index 0000000..d6576d4 --- /dev/null +++ b/pkg/grafanabackuper/restore.go @@ -0,0 +1,103 @@ +package grafanabackuper + +import ( + "context" + "fmt" + "io" + + "git.ar21.de/yolokube/grafana-backuper/internal/config" + "git.ar21.de/yolokube/grafana-backuper/pkg/grafana" +) + +type RestoreClient struct { + client *Client +} + +func (r *RestoreClient) Start(ctx context.Context, cfg *config.Config) error { + files, err := r.client.git.ListJSONFiles("./") + if err != nil { + return err + } + r.client.logger.Debug().Int("files", len(files)).Msg("Collected all files") + + for _, filename := range files { + r.client.logger.Debug().Str("file", filename).Msg("Reading data") + var data []byte + data, err = r.client.git.ReadFile(filename) + if err != nil { + return err + } + + r.client.logger.Debug().Msg("Parsing raw dashboard data") + var dashboard *grafana.Dashboard + dashboard, err = r.client.grafana.Dashboard.ParseRaw(data) + if err != nil { + return err + } + + content, ok := dashboard.Dashboard.(map[string]any) + if !ok { + continue + } + + uid := fmt.Sprint(content["uid"]) + title := fmt.Sprint(content["title"]) + + r.client.logger.Debug(). + Str("dashboard-uid", uid). + Str("title", title). + Msg("Fetching current dashboard version from Grafana") + var current *grafana.Dashboard + current, _, err = r.client.grafana.Dashboard.Get(ctx, uid) + if err != nil { + return err + } + + if current.Version == dashboard.Version && !cfg.ForceCommits { + r.client.logger.Info(). + Any("dashboard-uid", uid). + Str("folder", dashboard.FolderTitle). + Str("dashboard", title). + Msg("Dashboard already up to date") + continue + } + + r.client.logger.Debug().Str("dashboard-uid", uid).Str("title", title).Msg("Syncing dashboard with Grafana") + var createResp *grafana.DashboardCreateResponse + createResp, err = r.syncDashboard(ctx, dashboard, cfg.ForceCommits) + if err != nil { + return err + } + + r.client.logger.Info().Any("resp", createResp).Msg("Created / Updated dashboard successfully") + } + + return nil +} + +func (r *RestoreClient) syncDashboard( + ctx context.Context, + d *grafana.Dashboard, + force bool, +) (*grafana.DashboardCreateResponse, error) { + createOpts := grafana.DashboardCreateOpts{ + Dashboard: d.Dashboard, + FolderUID: d.FolderUID, + Message: "sync git repository to grafana", + Overwrite: force, + } + + r.client.logger.Debug().Msg("Validating create options") + if err := createOpts.Validate(); err != nil { + return nil, err + } + + createResp, resp, err := r.client.grafana.Dashboard.Create(ctx, createOpts) + if err != nil { + body, _ := io.ReadAll(resp.Body) + r.client.logger.Debug().Str("resp", string(body)).Msg("Got error during dashboard creation / update") + return nil, err + } + + return createResp, nil +} From 64a96bc8e2e79b8370459e7566e921228994c648 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 20:22:41 +0200 Subject: [PATCH 22/26] feat(internal): add quiet flag --- internal/cmd/root.go | 3 +++ internal/config/config.go | 1 + 2 files changed, 4 insertions(+) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index d2b06ea..896dc8e 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -32,6 +32,8 @@ func NewRootCommand(c *config.Config) *cobra.Command { zerolog.SetGlobalLevel(zerolog.InfoLevel) if c.Debug { zerolog.SetGlobalLevel(zerolog.DebugLevel) + } else if c.Quiet { + zerolog.SetGlobalLevel(zerolog.ErrorLevel) } if c.JSONFormat { @@ -47,6 +49,7 @@ func NewRootCommand(c *config.Config) *cobra.Command { 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") diff --git a/internal/config/config.go b/internal/config/config.go index 1b1ef53..331dcea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,7 @@ type Config struct { GitUser string GitPass string GPGKey string + Quiet bool Output io.Writer Logger zerolog.Logger From 316e5de75735a8f466d348ecbffd2166b1113da8 Mon Sep 17 00:00:00 2001 From: Aaron Riedel Date: Tue, 20 Aug 2024 20:21:17 +0200 Subject: [PATCH 23/26] add woodpecker pipeline --- .drone.yml | 124 ------------------------------------ .woodpecker/.build.yaml | 43 +++++++++++++ .woodpecker/.deploy.yaml | 57 +++++++++++++++++ .woodpecker/.gofmt.yaml | 7 ++ .woodpecker/.lint.yaml | 7 ++ .woodpecker/.vulncheck.yaml | 8 +++ 6 files changed, 122 insertions(+), 124 deletions(-) delete mode 100644 .drone.yml create mode 100644 .woodpecker/.build.yaml create mode 100644 .woodpecker/.deploy.yaml create mode 100644 .woodpecker/.gofmt.yaml create mode 100644 .woodpecker/.lint.yaml create mode 100644 .woodpecker/.vulncheck.yaml diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 30aa472..0000000 --- a/.drone.yml +++ /dev/null @@ -1,124 +0,0 @@ -kind: pipeline -name: build - -steps: -- name: gofmt - image: golang:1.22.5 - commands: - - gofmt -l -s . - when: - event: - - push -- name: golangci-linter - image: golangci/golangci-lint:v1.59.1 - commands: - - golangci-lint run ./... - 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: 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/.woodpecker/.build.yaml b/.woodpecker/.build.yaml new file mode 100644 index 0000000..bf7294e --- /dev/null +++ b/.woodpecker/.build.yaml @@ -0,0 +1,43 @@ +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: + - gofmt + - lint + - vulncheck diff --git a/.woodpecker/.deploy.yaml b/.woodpecker/.deploy.yaml new file mode 100644 index 0000000..e7bcfd9 --- /dev/null +++ b/.woodpecker/.deploy.yaml @@ -0,0 +1,57 @@ +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 \ No newline at end of file diff --git a/.woodpecker/.gofmt.yaml b/.woodpecker/.gofmt.yaml new file mode 100644 index 0000000..8db41f6 --- /dev/null +++ b/.woodpecker/.gofmt.yaml @@ -0,0 +1,7 @@ +steps: +- name: gofmt + image: golang:1.22.5 + commands: + - gofmt -l -s . + when: + - event: push diff --git a/.woodpecker/.lint.yaml b/.woodpecker/.lint.yaml new file mode 100644 index 0000000..d2477ad --- /dev/null +++ b/.woodpecker/.lint.yaml @@ -0,0 +1,7 @@ +steps: +- name: golangci-linter + image: golangci/golangci-lint:v1.59.1 + commands: + - golangci-lint run ./... + when: + - event: push diff --git a/.woodpecker/.vulncheck.yaml b/.woodpecker/.vulncheck.yaml new file mode 100644 index 0000000..2be22f8 --- /dev/null +++ b/.woodpecker/.vulncheck.yaml @@ -0,0 +1,8 @@ +steps: +- name: vuln-check + image: golang:1.22.5 + commands: + - go install golang.org/x/vuln/cmd/govulncheck@latest + - govulncheck ./... + when: + - event: push From 1356128a7bf239cb8f9fd471cb4de6d37e9e5c2c Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Fri, 6 Dec 2024 23:56:18 +0100 Subject: [PATCH 24/26] test(config,git,logger): add some test functions --- .golangci.yml | 24 +++- .woodpecker/.build.yaml | 3 +- .woodpecker/.deploy.yaml | 4 +- .woodpecker/.lint.yaml | 13 ++ .woodpecker/{.gofmt.yaml => .test.yaml} | 6 +- .woodpecker/.vulncheck.yaml | 8 -- go.mod | 3 + internal/config/config.go | 21 ++- internal/config/config_test.go | 40 ++++++ internal/logger/logger_test.go | 49 +++++++ pkg/git/project_test.go | 14 ++ pkg/git/sign_key_test.go | 36 +++++ pkg/git/testdata/test-empty-key.asc | 4 + pkg/git/testdata/test-invalid-key.asc | 1 + pkg/git/testdata/test-key.asc | 182 ++++++++++++++++++++++++ pkg/grafana/dashboard_version.go | 4 +- 16 files changed, 387 insertions(+), 25 deletions(-) rename .woodpecker/{.gofmt.yaml => .test.yaml} (54%) delete mode 100644 .woodpecker/.vulncheck.yaml create mode 100644 internal/config/config_test.go create mode 100644 internal/logger/logger_test.go create mode 100644 pkg/git/project_test.go create mode 100644 pkg/git/sign_key_test.go create mode 100644 pkg/git/testdata/test-empty-key.asc create mode 100644 pkg/git/testdata/test-invalid-key.asc create mode 100644 pkg/git/testdata/test-key.asc diff --git a/.golangci.yml b/.golangci.yml index 147fb08..c6b39dc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,16 @@ +# 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: 5m + timeout: 3m # This file contains only configs which differ from defaults. @@ -71,6 +80,11 @@ linters-settings: # 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) @@ -218,14 +232,13 @@ linters: - 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 + - 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 - - exportloopref # checks for pointers to enclosing loop variables - fatcontext # detects nested contexts in loops - forbidigo # forbids identifiers - funlen # tool for detection of long functions @@ -243,6 +256,7 @@ linters: - 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) @@ -264,6 +278,7 @@ linters: - 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 @@ -305,7 +320,7 @@ linters: #- dupword # [useless without config] checks for duplicate words in the source code #- err113 # [too strict] checks the errors handling expressions #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted - #- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds + #- 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 @@ -335,6 +350,7 @@ issues: linters: - bodyclose - dupl + - errcheck - funlen - goconst - gosec diff --git a/.woodpecker/.build.yaml b/.woodpecker/.build.yaml index bf7294e..60c67b4 100644 --- a/.woodpecker/.build.yaml +++ b/.woodpecker/.build.yaml @@ -38,6 +38,5 @@ steps: exclude: main event: push depends_on: - - gofmt - lint - - vulncheck + - test diff --git a/.woodpecker/.deploy.yaml b/.woodpecker/.deploy.yaml index e7bcfd9..ed45768 100644 --- a/.woodpecker/.deploy.yaml +++ b/.woodpecker/.deploy.yaml @@ -54,4 +54,6 @@ steps: - branch: main event: push depends_on: - - build \ No newline at end of file + - build + - lint + - test diff --git a/.woodpecker/.lint.yaml b/.woodpecker/.lint.yaml index d2477ad..8570785 100644 --- a/.woodpecker/.lint.yaml +++ b/.woodpecker/.lint.yaml @@ -1,4 +1,17 @@ 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.59.1 commands: diff --git a/.woodpecker/.gofmt.yaml b/.woodpecker/.test.yaml similarity index 54% rename from .woodpecker/.gofmt.yaml rename to .woodpecker/.test.yaml index 8db41f6..226568d 100644 --- a/.woodpecker/.gofmt.yaml +++ b/.woodpecker/.test.yaml @@ -1,7 +1,9 @@ steps: -- name: gofmt +- name: gotest image: golang:1.22.5 commands: - - gofmt -l -s . + - go test ./... when: - event: push +depends_on: + - lint diff --git a/.woodpecker/.vulncheck.yaml b/.woodpecker/.vulncheck.yaml deleted file mode 100644 index 2be22f8..0000000 --- a/.woodpecker/.vulncheck.yaml +++ /dev/null @@ -1,8 +0,0 @@ -steps: -- name: vuln-check - image: golang:1.22.5 - commands: - - go install golang.org/x/vuln/cmd/govulncheck@latest - - govulncheck ./... - when: - - event: push diff --git a/go.mod b/go.mod index 44497ab..6d4bb45 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( 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 ) @@ -18,6 +19,7 @@ require ( 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 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 @@ -32,6 +34,7 @@ require ( 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 diff --git a/internal/config/config.go b/internal/config/config.go index 331dcea..766f749 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,6 +7,15 @@ import ( "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 @@ -29,27 +38,27 @@ func (c *Config) Validate() []error { var errs []error if c.GrafanaURL == "" { - errs = append(errs, errors.New("invalid grafana URL, must not be blank")) + errs = append(errs, ErrInvalidGrafanaURL) } if c.GrafanaToken == "" { - errs = append(errs, errors.New("invalid auth token for grafana, must not be blank")) + errs = append(errs, ErrInvalidAuthToken) } if c.GitRepo == "" { - errs = append(errs, errors.New("invalid repo url for git, must not be blank")) + errs = append(errs, ErrInvalidRepoURL) } if c.GitUser == "" { - errs = append(errs, errors.New("invalid username for git, must not be blank")) + errs = append(errs, ErrInvalidGitUser) } if c.GitPass == "" { - errs = append(errs, errors.New("invalid password for git, must not be blank")) + errs = append(errs, ErrInvalidGitPass) } if c.GitBranch == "" { - errs = append(errs, errors.New("invalid branch name for git, must not be blank")) + errs = append(errs, ErrInvalidBranchName) } return errs diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..4102d67 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,40 @@ +package config_test + +import ( + "testing" + + "git.ar21.de/yolokube/grafana-backuper/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestConfig_Validate_MissingFields(t *testing.T) { + cfg := config.Config{} + + errs := cfg.Validate() + assert.Len(t, errs, 6) // Expecting 6 errors since all required fields are missing +} + +func TestConfig_Validate_AllFieldsPresent(t *testing.T) { + cfg := config.Config{ + GrafanaURL: "http://grafana.example.com", + GrafanaToken: "sometoken", + GitRepo: "https://github.com/user/repo", + GitUser: "username", + GitPass: "password", + GitBranch: "main", + } + + errs := cfg.Validate() + assert.Empty(t, errs) // No errors should be returned when all fields are valid +} + +func TestConfig_Validate_PartiallyPopulated(t *testing.T) { + cfg := config.Config{ + GrafanaURL: "http://grafana.example.com", + GitRepo: "https://github.com/user/repo", + GitUser: "username", + } + + errs := cfg.Validate() + assert.Len(t, errs, 3) // Expecting 3 errors for missing GrafanaToken, GitPass, and GitBranch +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 0000000..34a3766 --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,49 @@ +package logger_test + +import ( + "bytes" + "testing" + + "git.ar21.de/yolokube/grafana-backuper/internal/logger" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func TestCliLoggerLayout_DebugLevel(t *testing.T) { + var buf bytes.Buffer + zerolog.SetGlobalLevel(zerolog.DebugLevel) + + log := logger.CliLoggerLayout(&buf) + log.Debug().Msg("test message") + + output := buf.String() + assert.Contains(t, output, "test message") + assert.Contains(t, output, "DBG") // Assuming zerolog adds a short level indicator like "DBG" + assert.Contains(t, output, ".go:") // Ensures the caller file and line are included +} + +func TestCliLoggerLayout_NonDebugLevel(t *testing.T) { + var buf bytes.Buffer + zerolog.SetGlobalLevel(zerolog.InfoLevel) + + log := logger.CliLoggerLayout(&buf) + log.Info().Msg("test message") + + output := buf.String() + assert.Contains(t, output, "test message") + assert.NotContains(t, output, "DBG") // Since we're at Info level + assert.NotContains(t, output, ".go:") // Caller information should not be included +} + +func TestJSONLoggerLayout(t *testing.T) { + var buf bytes.Buffer + + log := logger.JSONLoggerLayout(&buf) + log.Info().Msg("test message") + + output := buf.String() + assert.Contains(t, output, `"message":"test message"`) + assert.Contains(t, output, `"level":"info"`) + assert.Contains(t, output, `"time":`) // Timestamp should be included + assert.Contains(t, output, `"caller":`) // Caller should be included +} diff --git a/pkg/git/project_test.go b/pkg/git/project_test.go new file mode 100644 index 0000000..5f3f92c --- /dev/null +++ b/pkg/git/project_test.go @@ -0,0 +1,14 @@ +package git_test + +import ( + "testing" + + "git.ar21.de/yolokube/grafana-backuper/pkg/git" + "github.com/stretchr/testify/assert" +) + +func TestNewProject(t *testing.T) { + project := git.NewProject("https://example.com/repo.git") + assert.NotNil(t, project) + assert.Equal(t, "https://example.com/repo.git", project.RepoURL) +} diff --git a/pkg/git/sign_key_test.go b/pkg/git/sign_key_test.go new file mode 100644 index 0000000..ef71347 --- /dev/null +++ b/pkg/git/sign_key_test.go @@ -0,0 +1,36 @@ +package git_test + +import ( + "testing" + + "git.ar21.de/yolokube/grafana-backuper/pkg/git" + "github.com/stretchr/testify/assert" +) + +func TestReadKeyFile_Success(t *testing.T) { + signKey := &git.SignKey{KeyFile: "testdata/test-key.asc"} + err := signKey.ReadKeyFile() + + assert.NoError(t, err) +} + +func TestReadKeyFile_FileError(t *testing.T) { + signKey := &git.SignKey{KeyFile: "nonexistent-file.asc"} + err := signKey.ReadKeyFile() + + assert.Error(t, err) +} + +func TestReadKeyFile_DecodingError(t *testing.T) { + signKey := &git.SignKey{KeyFile: "testdata/test-invalid-key.asc"} + err := signKey.ReadKeyFile() + + assert.Error(t, err) +} + +func TestReadKeyFile_EmptyKeyRing(t *testing.T) { + signKey := &git.SignKey{KeyFile: "testdata/test-empty-key.asc"} + err := signKey.ReadKeyFile() + + assert.Error(t, err) +} diff --git a/pkg/git/testdata/test-empty-key.asc b/pkg/git/testdata/test-empty-key.asc new file mode 100644 index 0000000..00e9653 --- /dev/null +++ b/pkg/git/testdata/test-empty-key.asc @@ -0,0 +1,4 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: Keybase OpenPGP v1.0.0 +Comment: https://keybase.io/crypto +-----END PGP PRIVATE KEY BLOCK----- diff --git a/pkg/git/testdata/test-invalid-key.asc b/pkg/git/testdata/test-invalid-key.asc new file mode 100644 index 0000000..8d9c88f --- /dev/null +++ b/pkg/git/testdata/test-invalid-key.asc @@ -0,0 +1 @@ +invalid content diff --git a/pkg/git/testdata/test-key.asc b/pkg/git/testdata/test-key.asc new file mode 100644 index 0000000..f2fcb86 --- /dev/null +++ b/pkg/git/testdata/test-key.asc @@ -0,0 +1,182 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: Keybase OpenPGP v1.0.0 +Comment: https://keybase.io/crypto + +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 +YZ3g9Y8QOzszYPB450rVxLOEY6eV1Q== +=W3yz +-----END PGP PRIVATE KEY BLOCK----- diff --git a/pkg/grafana/dashboard_version.go b/pkg/grafana/dashboard_version.go index fa0404f..d3c6a96 100644 --- a/pkg/grafana/dashboard_version.go +++ b/pkg/grafana/dashboard_version.go @@ -74,7 +74,7 @@ func (c *DashboardVersionClient) Get( version uint, params ...DashboardVersionParam, ) (*DashboardVersion, *Response, error) { - if id, err := strconv.Atoi(input); err == nil { + 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...) @@ -111,7 +111,7 @@ func (c *DashboardVersionClient) List( input string, params ...DashboardVersionParam, ) ([]*DashboardVersion, *Response, error) { - if id, err := strconv.Atoi(input); err == nil { + if id, err := strconv.ParseUint(input, 10, 64); err == nil { return c.ListByID(ctx, uint(id), params...) } return c.ListByUID(ctx, input, params...) From 30ee41171a04fe82b0bec7345bfe9bbe19c6525e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 25 Nov 2024 15:04:34 +0000 Subject: [PATCH 25/26] chore(deps): update golangci/golangci-lint docker tag to v1.62.2 --- .woodpecker/.lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/.lint.yaml b/.woodpecker/.lint.yaml index 8570785..c4f1dc3 100644 --- a/.woodpecker/.lint.yaml +++ b/.woodpecker/.lint.yaml @@ -13,7 +13,7 @@ steps: when: - event: push - name: golangci-linter - image: golangci/golangci-lint:v1.59.1 + image: golangci/golangci-lint:v1.62.2 commands: - golangci-lint run ./... when: From 579f2a5df76c92fffbe7c664e269ff8b18a29c0c Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sat, 7 Dec 2024 00:52:25 +0100 Subject: [PATCH 26/26] feat(cmd): add `sequence` flag to execute multiple functions --- Dockerfile | 12 ++++++++++-- entrypoint.sh | 8 ++++++++ internal/cmd/backup.go | 2 -- internal/cmd/root.go | 18 ++++++++++++++++++ internal/config/config.go | 1 + 5 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index d25a8ac..ee958ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,14 @@ FROM alpine WORKDIR /app # Copy built binary from build image -COPY --from=build /workspace/grafana-backuper /app +COPY --from=build /workspace/grafana-backuper . -ENTRYPOINT ["/app/grafana-backuper backup --json"] +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"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..b5a87a3 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Check if the environment variable GRAFANA_MODE is set +if [ -z "$GB_SEQUENCE" ]; then + exec ./grafana-backuper backup --json +else + exec ./grafana-backuper --json +fi diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index fc7629c..dac2628 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -37,8 +37,6 @@ func NewBackupCommand(c *config.Config) *cobra.Command { } backupCmd.PersistentFlags().BoolVar(&c.ForceCommits, "force", false, "Force git commits / ignore existence check") - backupCmd.PersistentFlags().StringVar(&c.GitEmail, "git-email", "", "Git email address") - backupCmd.PersistentFlags().StringVar(&c.GPGKey, "signing-key", "", "Path to the GPG signing key") return backupCmd } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 896dc8e..4d5fb05 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -23,6 +23,21 @@ func NewRootCommand(c *config.Config) *cobra.Command { 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) @@ -56,6 +71,9 @@ func NewRootCommand(c *config.Config) *cobra.Command { 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 } diff --git a/internal/config/config.go b/internal/config/config.go index 766f749..f95cb6a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,6 +29,7 @@ type Config struct { GitPass string GPGKey string Quiet bool + Sequence []string Output io.Writer Logger zerolog.Logger