Files
grafana/apps/provisioning/pkg/repository/github/repository.go
Roberto Jiménez Sánchez 4eadc823a9 Provisioning: Move repository package to provisioning app (#110228)
* Move repository package to apps

* Move operators to grafana/grafana

* Go mod tidy

* Own package by git sync team for now

* Merged

* Do not use settings in local extra

* Remove dependency on webhook extra

* Hack to work around issue with secure contracts

* Sync Go modules

* Revert "Move operators to grafana/grafana"

This reverts commit 9f19b30a2e.
2025-09-02 09:45:44 +02:00

237 lines
6.4 KiB
Go

package github
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"k8s.io/apimachinery/pkg/util/validation/field"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/grafana/grafana/apps/provisioning/pkg/repository/git"
"github.com/grafana/grafana/apps/provisioning/pkg/safepath"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included.
type githubRepository struct {
git.GitRepository
config *provisioning.Repository
gh Client // assumes github.com base URL
owner string
repo string
}
// GithubRepository is an interface that combines all repository capabilities
// needed for GitHub repositories.
//go:generate mockery --name GithubRepository --structname MockGithubRepository --inpackage --filename github_repository_mock.go --with-expecter
type GithubRepository interface {
repository.Repository
repository.Versioned
repository.Writer
repository.Reader
repository.RepositoryWithURLs
repository.StageableRepository
Owner() string
Repo() string
Client() Client
}
func NewRepository(
ctx context.Context,
config *provisioning.Repository,
gitRepo git.GitRepository,
factory *Factory,
token common.RawSecureValue,
) (GithubRepository, error) {
owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL)
if err != nil {
return nil, fmt.Errorf("parse owner and repo: %w", err)
}
return &githubRepository{
config: config,
GitRepository: gitRepo,
gh: factory.New(ctx, token), // TODO, baseURL from config
owner: owner,
repo: repo,
}, nil
}
func (r *githubRepository) Owner() string {
return r.owner
}
func (r *githubRepository) Repo() string {
return r.repo
}
func (r *githubRepository) Client() Client {
return r.gh
}
// Validate implements provisioning.Repository.
func (r *githubRepository) Validate() (list field.ErrorList) {
cfg := r.Config()
gh := cfg.Spec.GitHub
if gh == nil {
list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required"))
return list
}
if gh.URL == "" {
list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required"))
} else {
_, _, err := ParseOwnerRepoGithub(gh.URL)
if err != nil {
list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error()))
} else if !strings.HasPrefix(gh.URL, "https://github.com/") {
list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/"))
}
}
if len(list) > 0 {
return list
}
return r.GitRepository.Validate()
}
func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) {
giturl = strings.TrimSuffix(giturl, ".git")
giturl = strings.TrimSuffix(giturl, "/")
parsed, e := url.Parse(giturl)
if e != nil {
err = e
return
}
parts := strings.Split(parsed.Path, "/")
if len(parts) < 3 {
err = fmt.Errorf("unable to parse repo+owner from url")
return
}
return parts[1], parts[2], nil
}
// Test implements provisioning.Repository.
func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) {
url := r.config.Spec.GitHub.URL
_, _, err := ParseOwnerRepoGithub(url)
if err != nil {
return repository.FromFieldError(field.Invalid(
field.NewPath("spec", "github", "url"), url, err.Error())), nil
}
return r.GitRepository.Test(ctx)
}
func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
finalPath := safepath.Join(r.config.Spec.GitHub.Path, path)
commits, err := r.gh.Commits(ctx, r.owner, r.repo, finalPath, ref)
if err != nil {
if errors.Is(err, ErrResourceNotFound) {
return nil, repository.ErrFileNotFound
}
return nil, fmt.Errorf("get commits: %w", err)
}
ret := make([]provisioning.HistoryItem, 0, len(commits))
for _, commit := range commits {
authors := make([]provisioning.Author, 0)
if commit.Author != nil {
authors = append(authors, provisioning.Author{
Name: commit.Author.Name,
Username: commit.Author.Username,
AvatarURL: commit.Author.AvatarURL,
})
}
if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name {
authors = append(authors, provisioning.Author{
Name: commit.Committer.Name,
Username: commit.Committer.Username,
AvatarURL: commit.Committer.AvatarURL,
})
}
ret = append(ret, provisioning.HistoryItem{
Ref: commit.Ref,
Message: commit.Message,
Authors: authors,
CreatedAt: commit.CreatedAt.UnixMilli(),
})
}
return ret, nil
}
// ListRefs list refs from the git repository and add the ref URL to the ref item
func (r *githubRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) {
refs, err := r.GitRepository.ListRefs(ctx)
if err != nil {
return nil, fmt.Errorf("list refs: %w", err)
}
for i := range refs {
refs[i].RefURL = fmt.Sprintf("%s/tree/%s", r.config.Spec.GitHub.URL, refs[i].Name)
}
return refs, nil
}
// ResourceURLs implements RepositoryWithURLs.
func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*provisioning.RepositoryURLs, error) {
cfg := r.config.Spec.GitHub
if file.Path == "" || cfg == nil {
return nil, nil
}
ref := file.Ref
if ref == "" {
ref = cfg.Branch
}
urls := &provisioning.RepositoryURLs{
RepositoryURL: cfg.URL,
SourceURL: fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path),
}
if ref != cfg.Branch {
urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref)
// Create a new pull request
urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL)
}
return urls, nil
}
// RefURLs implements RepositoryWithURLs.
func (r *githubRepository) RefURLs(ctx context.Context, ref string) (*provisioning.RepositoryURLs, error) {
cfg := r.config.Spec.GitHub
if cfg == nil || ref == "" {
return nil, nil
}
urls := &provisioning.RepositoryURLs{
SourceURL: fmt.Sprintf("%s/tree/%s", cfg.URL, ref),
}
if ref != cfg.Branch {
urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref)
urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL)
}
return urls, nil
}