mirror of
https://github.com/grafana/grafana.git
synced 2026-08-18 17:15:08 -05:00
Storage: support git + github backed roots (#52192)
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/infra/filestorage"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"gocloud.dev/blob"
|
||||
)
|
||||
|
||||
const rootStorageTypeGit = "git"
|
||||
|
||||
var _ storageRuntime = &rootStorageGit{}
|
||||
|
||||
type rootStorageGit struct {
|
||||
settings *StorageGitConfig
|
||||
repo *git.Repository
|
||||
root string // repostitory root
|
||||
|
||||
github *githubHelper
|
||||
meta RootStorageMeta
|
||||
store filestorage.FileStorage
|
||||
}
|
||||
|
||||
func newGitStorage(meta RootStorageMeta, scfg RootStorageConfig, localWorkCache string) *rootStorageGit {
|
||||
cfg := scfg.Git
|
||||
if cfg == nil {
|
||||
cfg = &StorageGitConfig{}
|
||||
}
|
||||
scfg.Type = rootStorageTypeGit
|
||||
scfg.GCS = nil
|
||||
scfg.SQL = nil
|
||||
scfg.S3 = nil
|
||||
scfg.Git = cfg
|
||||
|
||||
meta.Config = scfg
|
||||
if scfg.Prefix == "" {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "Missing prefix",
|
||||
})
|
||||
}
|
||||
if cfg.Remote == "" {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "Missing remote path configuration",
|
||||
})
|
||||
}
|
||||
|
||||
if len(localWorkCache) < 2 {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "Invalid local root folder",
|
||||
})
|
||||
}
|
||||
|
||||
s := &rootStorageGit{
|
||||
settings: cfg,
|
||||
}
|
||||
if meta.Notice == nil {
|
||||
err := os.MkdirAll(localWorkCache, 0750)
|
||||
if err != nil {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if scfg.Disabled {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityWarning,
|
||||
Text: "folder is disabled (in configuration)",
|
||||
})
|
||||
} else if setting.Env == setting.Prod {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "git is only supported in dev mode (for now)",
|
||||
})
|
||||
}
|
||||
|
||||
if meta.Notice == nil {
|
||||
repo, err := git.PlainOpen(localWorkCache)
|
||||
if errors.Is(err, git.ErrRepositoryNotExists) {
|
||||
repo, err = git.PlainClone(localWorkCache, false, &git.CloneOptions{
|
||||
URL: cfg.Remote,
|
||||
Progress: os.Stdout,
|
||||
//Depth: 1,
|
||||
//SingleBranch: true,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
p := localWorkCache
|
||||
if cfg.Root != "" {
|
||||
p = filepath.Join(p, cfg.Root)
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("file://%s", p)
|
||||
bucket, err := blob.OpenBucket(context.Background(), path)
|
||||
if err != nil {
|
||||
grafanaStorageLogger.Warn("error loading storage", "prefix", scfg.Prefix, "err", err)
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "Failed to initialize storage",
|
||||
})
|
||||
} else {
|
||||
s.store = filestorage.NewCdkBlobStorage(
|
||||
grafanaStorageLogger,
|
||||
bucket, "", nil)
|
||||
|
||||
meta.Ready = true // exists!
|
||||
s.root = p
|
||||
|
||||
token := cfg.AccessToken
|
||||
if strings.HasPrefix(token, "$") {
|
||||
token = os.Getenv(token[1:])
|
||||
if token == "" {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "Unable to find token environment variable: " + cfg.AccessToken,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
s.github, err = newGithubHelper(context.Background(), cfg.Remote, token)
|
||||
if err != nil {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "error creating github client: " + err.Error(),
|
||||
})
|
||||
s.github = nil
|
||||
} else {
|
||||
ghrepo, _, err := s.github.getRepo(context.Background())
|
||||
if err != nil {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: err.Error(),
|
||||
})
|
||||
s.github = nil
|
||||
} else {
|
||||
grafanaStorageLogger.Info("default branch", "branch", *ghrepo.DefaultBranch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.repo = repo
|
||||
|
||||
// Try pulling after init
|
||||
if s.repo != nil && !scfg.Disabled {
|
||||
err = s.Sync()
|
||||
if err != nil {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "unable to pull: " + err.Error(),
|
||||
})
|
||||
} else if cfg.PullInterval != "" {
|
||||
t, err := time.ParseDuration(cfg.PullInterval)
|
||||
if err != nil {
|
||||
meta.Notice = append(meta.Notice, data.Notice{
|
||||
Severity: data.NoticeSeverityError,
|
||||
Text: "Invalid pull interval " + cfg.PullInterval,
|
||||
})
|
||||
} else {
|
||||
ticker := time.NewTicker(t)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
grafanaStorageLogger.Info("try git pull", "branch", s.settings.Remote)
|
||||
err = s.Sync()
|
||||
if err != nil {
|
||||
grafanaStorageLogger.Info("error pulling", "error", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.meta = meta
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *rootStorageGit) Meta() RootStorageMeta {
|
||||
return s.meta
|
||||
}
|
||||
|
||||
func (s *rootStorageGit) Store() filestorage.FileStorage {
|
||||
return s.store
|
||||
}
|
||||
|
||||
func (s *rootStorageGit) Pull() error {
|
||||
w, err := s.repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.Pull(&git.PullOptions{
|
||||
// Depth: 1,
|
||||
//SingleBranch: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rootStorageGit) Write(ctx context.Context, cmd *WriteValueRequest) (*WriteValueResponse, error) {
|
||||
if s.github == nil {
|
||||
return nil, fmt.Errorf("github client not initialized")
|
||||
}
|
||||
// Write to the correct subfolder
|
||||
if s.settings.Root != "" {
|
||||
cmd.Path = s.settings.Root + cmd.Path
|
||||
}
|
||||
|
||||
if cmd.Workflow == WriteValueWorkflow_PR {
|
||||
prcmd := makePRCommand{
|
||||
baseBranch: s.settings.Branch,
|
||||
headBranch: fmt.Sprintf("grafana_ui_%d", time.Now().UnixMilli()),
|
||||
title: cmd.Title,
|
||||
body: cmd.Message,
|
||||
}
|
||||
res := &WriteValueResponse{
|
||||
Branch: prcmd.headBranch,
|
||||
}
|
||||
|
||||
ref, _, err := s.github.createRef(ctx, prcmd.baseBranch, prcmd.headBranch)
|
||||
if err != nil {
|
||||
res.Code = 500
|
||||
res.Message = "unable to create branch"
|
||||
return res, nil
|
||||
}
|
||||
|
||||
err = s.github.pushCommit(ctx, ref, cmd)
|
||||
if err != nil {
|
||||
res.Code = 500
|
||||
res.Message = fmt.Sprintf("error creating commit: %s", err.Error())
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if prcmd.title == "" {
|
||||
prcmd.title = "Dashboard save: " + time.Now().String()
|
||||
}
|
||||
if prcmd.body == "" {
|
||||
prcmd.body = "Dashboard save: " + time.Now().String()
|
||||
}
|
||||
|
||||
pr, _, err := s.github.createPR(ctx, prcmd)
|
||||
if err != nil {
|
||||
res.Code = 500
|
||||
res.Message = "error creating PR: " + err.Error()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res.Code = 200
|
||||
res.URL = pr.GetHTMLURL()
|
||||
res.Pending = true
|
||||
res.Hash = *ref.Object.SHA
|
||||
res.Branch = prcmd.headBranch
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Push to remote branch (save)
|
||||
if cmd.Workflow == WriteValueWorkflow_Push || true {
|
||||
res := &WriteValueResponse{
|
||||
Branch: s.settings.Branch,
|
||||
}
|
||||
ref, _, err := s.github.getRef(ctx, s.settings.Branch)
|
||||
if err != nil {
|
||||
res.Code = 500
|
||||
res.Message = "unable to create branch"
|
||||
return res, nil
|
||||
}
|
||||
err = s.github.pushCommit(ctx, ref, cmd)
|
||||
if err != nil {
|
||||
res.Code = 500
|
||||
res.Message = "error creating commit"
|
||||
return res, nil
|
||||
}
|
||||
ref, _, _ = s.github.getRef(ctx, s.settings.Branch)
|
||||
if ref != nil {
|
||||
res.Hash = *ref.Object.SHA
|
||||
res.URL = ref.GetURL()
|
||||
}
|
||||
|
||||
err = s.Pull()
|
||||
if err != nil {
|
||||
res.Message = "error pulling: " + err.Error()
|
||||
}
|
||||
|
||||
res.Code = 200
|
||||
return res, nil
|
||||
}
|
||||
|
||||
rel := cmd.Path
|
||||
if s.meta.Config.Git.Root != "" {
|
||||
rel = filepath.Join(s.meta.Config.Git.Root, cmd.Path)
|
||||
}
|
||||
|
||||
fpath := filepath.Join(s.root, rel)
|
||||
err := os.WriteFile(fpath, cmd.Body, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, err := s.repo.Worktree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The file we just wrote
|
||||
_, err = w.Add(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg := cmd.Message
|
||||
if msg == "" {
|
||||
msg = "changes from grafana ui"
|
||||
}
|
||||
user := cmd.User
|
||||
if user == nil {
|
||||
user = &models.SignedInUser{}
|
||||
}
|
||||
|
||||
hash, err := w.Commit(msg, &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: firstRealString(user.Name, user.Login, user.Email, "?"),
|
||||
Email: firstRealString(user.Email, user.Login, user.Name, "?"),
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
grafanaStorageLogger.Info("made commit", "hash", hash)
|
||||
// err = s.repo.Push(&git.PushOptions{
|
||||
// InsecureSkipTLS: true,
|
||||
// })
|
||||
|
||||
return &WriteValueResponse{
|
||||
Hash: hash.String(),
|
||||
Message: "made commit",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *rootStorageGit) Sync() error {
|
||||
grafanaStorageLogger.Info("GIT PULL", "remote", s.settings.Remote)
|
||||
err := s.Pull()
|
||||
if err != nil {
|
||||
if err.Error() == "already up-to-date" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func firstRealString(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
Reference in New Issue
Block a user