From 453041b4b23ec216436d133434b25932a630228c Mon Sep 17 00:00:00 2001 From: Tom Neuber Date: Mon, 19 Aug 2024 14:08:51 +0200 Subject: [PATCH] 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 +}