grafana/pkg/services/store/storage_git.go
idafurjes 6afad51761
Move SignedInUser to user service and RoleType and Roles to org (#53445)
* Move SignedInUser to user service and RoleType and Roles to org

* Use go naming convention for roles

* Fix some imports and leftovers

* Fix ldap debug test

* Fix lint

* Fix lint 2

* Fix lint 3

* Fix type and not needed conversion

* Clean up messages in api tests

* Clean up api tests 2
2022-08-10 11:56:48 +02:00

388 lines
8.7 KiB
Go

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/services/user"
"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"
}
usr := cmd.User
if usr == nil {
usr = &user.SignedInUser{}
}
hash, err := w.Commit(msg, &git.CommitOptions{
Author: &object.Signature{
Name: firstRealString(usr.Name, usr.Login, usr.Email, "?"),
Email: firstRealString(usr.Email, usr.Login, usr.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 "?"
}