From 153e452e4f49ba91cfa48d902620b25f5eae0eeb Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 20:12:28 +0200 Subject: [PATCH 01/12] refactor(grafanabackuper): add restore function --- cmd/main.go | 5 +- internal/cmd/restore.go | 53 ++++++++++++++++ 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 ++++++++++++++++++++++++++++++++ 8 files changed, 212 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/restore.go b/internal/cmd/restore.go new file mode 100644 index 0000000..20d307d --- /dev/null +++ b/internal/cmd/restore.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "context" + "os" + + "git.ar21.de/yolokube/grafana-backuper/internal/config" + "git.ar21.de/yolokube/grafana-backuper/pkg/grafanabackuper" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +func NewRestoreCommand(c *config.Config) *cobra.Command { + backupCmd := &cobra.Command{ + Use: "restore", + Short: "Restore the dashboards from a git repository to grafana.", + Long: "Restore the dashboards from a git repository to grafana.", + Run: func(cmd *cobra.Command, _ []string) { + if err := restore(cmd.Context(), c); err != nil { + log.Fatal().Err(err).Send() + } + }, + PreRun: func(cmd *cobra.Command, _ []string) { + errs := c.Validate() + for _, err := range errs { + log.Error().Err(err).Send() + } + + if len(errs) > 0 { + if err := cmd.Help(); err != nil { + log.Error().Err(err).Send() + } + os.Exit(1) + } + }, + } + + backupCmd.PersistentFlags().BoolVar(&c.ForceCommits, "force", false, "Force dashboards / ignore existence check") + + return backupCmd +} + +func restore(ctx context.Context, cfg *config.Config) error { + client := grafanabackuper.NewClient( + grafanabackuper.WithZerologLogger(cfg.Logger), + ) + + if err := client.Prepare(ctx, cfg); err != nil { + return err + } + + return client.Restore.Start(ctx, cfg) +} 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 535ed5a5c69f3f8d6c86c909646c882434e42680 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 20:22:41 +0200 Subject: [PATCH 02/12] 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 066f8d764e3ba49ec568cf1ee5446bf028ff96f6 Mon Sep 17 00:00:00 2001 From: Aaron Riedel Date: Tue, 20 Aug 2024 20:21:17 +0200 Subject: [PATCH 03/12] add woodpecker pipeline --- .drone.yml | 124 ------------------------------------ .woodpecker/.build.yaml | 43 +++++++++++++ .woodpecker/.deploy.yaml | 27 ++++++++ .woodpecker/.gofmt.yaml | 7 ++ .woodpecker/.lint.yaml | 7 ++ .woodpecker/.vulncheck.yaml | 8 +++ 6 files changed, 92 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..0ca4b43 --- /dev/null +++ b/.woodpecker/.deploy.yaml @@ -0,0 +1,27 @@ +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/grafana-backuper-deployment.git deployment-repo + - cd deployment-repo/overlay + - kustomize edit set image git.ar21.de/yolokube/grafana-backuper=git.ar21.de/yolokube/grafana-backuper:${CI_PIPELINE_NUMBER} + when: + - branch: main + event: push +- name: push new tag to deployment-repo + image: appleboy/drone-git-push + settings: + branch: main + remote: ssh://git@git.ar21.de:2222/yolokube/grafana-backuper-deployment.git + path: deployment-repo + force: false + commit: true + commit_message: "GRAFANA-BACKUPER: update image tag to ${CI_PIPELINE_NUMBER} (done automagically via Woodpecker pipeline)" + ssh_key: + from_secret: FORGEJO_SSH_KEY + when: + - branch: main + event: push +depends_on: + - build \ 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 0c7af690875ec45dfe9812d7b5aa5e2407e27067 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Fri, 6 Dec 2024 23:56:18 +0100 Subject: [PATCH 04/12] 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 0ca4b43..2e43ad6 100644 --- a/.woodpecker/.deploy.yaml +++ b/.woodpecker/.deploy.yaml @@ -24,4 +24,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 f9018dd392dd0e4d2a70bd2c66833cd709df665a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 25 Nov 2024 15:04:34 +0000 Subject: [PATCH 05/12] 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 c8f52b5af13a24c5f13f5b336dbf88497729867c Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sat, 7 Dec 2024 00:52:25 +0100 Subject: [PATCH 06/12] feat(cmd): add `sequence` flag to execute multiple functions --- Dockerfile | 12 ++++++++++-- entrypoint.sh | 8 ++++++++ internal/cmd/backup.go | 2 -- internal/cmd/root.go | 17 +++++++++++++++++ internal/config/config.go | 1 + 5 files changed, 36 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..9a7bc3e --- /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 /app/grafana-backuper backup --json +else + exec /app/grafana-backuper --json +fi diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index e06c9fe..e147abd 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -36,8 +36,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..1f83831 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -23,6 +23,20 @@ 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 { + cmd.SetArgs([]string{command}) + if err := cmd.Execute(); err != nil { + return err + } + } + + return nil + }, PersistentPreRun: func(cmd *cobra.Command, _ []string) { initializeConfig(cmd) @@ -56,6 +70,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().StringArrayVar(&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 From b14c22f4a23f0ef1935a63ee71760e0189e37d6c Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 20:12:28 +0200 Subject: [PATCH 07/12] refactor(grafanabackuper): add restore function --- cmd/main.go | 5 +- internal/cmd/restore.go | 53 ++++++++++++++++ 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 ++++++++++++++++++++++++++++++++ 8 files changed, 212 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/restore.go b/internal/cmd/restore.go new file mode 100644 index 0000000..72ed201 --- /dev/null +++ b/internal/cmd/restore.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "context" + "os" + + "git.ar21.de/yolokube/grafana-backuper/internal/config" + "git.ar21.de/yolokube/grafana-backuper/pkg/grafanabackuper" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +func NewRestoreCommand(c *config.Config) *cobra.Command { + 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 75e164baace03d24758421e36947865fe54fdc25 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 20:22:41 +0200 Subject: [PATCH 08/12] 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 b19cf01534fe837060090aacdc21efea9afef051 Mon Sep 17 00:00:00 2001 From: Aaron Riedel Date: Tue, 20 Aug 2024 20:21:17 +0200 Subject: [PATCH 09/12] add woodpecker pipeline --- .drone.yml | 124 ------------------------------------ .woodpecker/.build.yaml | 43 +++++++++++++ .woodpecker/.deploy.yaml | 27 ++++++++ .woodpecker/.gofmt.yaml | 7 ++ .woodpecker/.lint.yaml | 7 ++ .woodpecker/.vulncheck.yaml | 8 +++ 6 files changed, 92 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..0ca4b43 --- /dev/null +++ b/.woodpecker/.deploy.yaml @@ -0,0 +1,27 @@ +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/grafana-backuper-deployment.git deployment-repo + - cd deployment-repo/overlay + - kustomize edit set image git.ar21.de/yolokube/grafana-backuper=git.ar21.de/yolokube/grafana-backuper:${CI_PIPELINE_NUMBER} + when: + - branch: main + event: push +- name: push new tag to deployment-repo + image: appleboy/drone-git-push + settings: + branch: main + remote: ssh://git@git.ar21.de:2222/yolokube/grafana-backuper-deployment.git + path: deployment-repo + force: false + commit: true + commit_message: "GRAFANA-BACKUPER: update image tag to ${CI_PIPELINE_NUMBER} (done automagically via Woodpecker pipeline)" + ssh_key: + from_secret: FORGEJO_SSH_KEY + when: + - branch: main + event: push +depends_on: + - build \ 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 da8b10f7b82e0764b12d4c0348d81746e3f4125a Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Fri, 6 Dec 2024 23:56:18 +0100 Subject: [PATCH 10/12] 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 0ca4b43..2e43ad6 100644 --- a/.woodpecker/.deploy.yaml +++ b/.woodpecker/.deploy.yaml @@ -24,4 +24,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 91df45ed5faeff61b328ae0193956e68eb4a4539 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 25 Nov 2024 15:04:34 +0000 Subject: [PATCH 11/12] 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 6302de004f85f3456ef15ff33070c1f54b9f92c2 Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Sat, 7 Dec 2024 00:52:25 +0100 Subject: [PATCH 12/12] feat(cmd): add `sequence` flag to execute multiple functions --- Dockerfile | 12 ++++++++++-- entrypoint.sh | 8 ++++++++ internal/cmd/backup.go | 2 -- internal/cmd/root.go | 17 +++++++++++++++++ internal/config/config.go | 1 + 5 files changed, 36 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..9a7bc3e --- /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 /app/grafana-backuper backup --json +else + exec /app/grafana-backuper --json +fi diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index e06c9fe..e147abd 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -36,8 +36,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..1f83831 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -23,6 +23,20 @@ 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 { + cmd.SetArgs([]string{command}) + if err := cmd.Execute(); err != nil { + return err + } + } + + return nil + }, PersistentPreRun: func(cmd *cobra.Command, _ []string) { initializeConfig(cmd) @@ -56,6 +70,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().StringArrayVar(&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