Provisioning: Use Nanogit for basic git operations in Github repository type (#107889)

This commit is contained in:
Roberto Jiménez Sánchez
2025-07-10 09:46:38 -07:00
committed by GitHub
parent 9df15d120d
commit 7e0848294e
75 changed files with 2426 additions and 12362 deletions
@@ -11,9 +11,8 @@ import (
provisioningapis "github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/nanogit"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest"
@@ -180,42 +179,41 @@ func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Reposit
gvr.Resource,
r.GetName(),
)
cloneFn := func(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) {
return gogit.Clone(ctx, e.clonedir, r, opts, e.secrets)
}
apiRepo, err := repository.NewGitHub(ctx, r, e.ghFactory, e.secrets, cloneFn)
if err != nil {
return nil, fmt.Errorf("create github API repository: %w", err)
}
logger := logging.FromContext(ctx).With("url", r.Spec.GitHub.URL, "branch", r.Spec.GitHub.Branch, "path", r.Spec.GitHub.Path)
if !e.features.IsEnabledGlobally(featuremgmt.FlagNanoGit) {
logger.Debug("Instantiating Github repository with go-git and Github API")
return NewGithubWebhookRepository(apiRepo, webhookURL, e.secrets), nil
}
logger.Info("Instantiating Github repository with nanogit")
logger.Info("Instantiating Github repository with webhooks")
ghCfg := r.Spec.GitHub
if ghCfg == nil {
return nil, fmt.Errorf("github configuration is required for nano git")
}
gitCfg := nanogit.RepositoryConfig{
// Decrypt GitHub token if needed
ghToken := ghCfg.Token
if ghToken == "" && len(ghCfg.EncryptedToken) > 0 {
decrypted, err := e.secrets.Decrypt(ctx, ghCfg.EncryptedToken)
if err != nil {
return nil, fmt.Errorf("decrypt github token: %w", err)
}
ghToken = string(decrypted)
}
gitCfg := git.RepositoryConfig{
URL: ghCfg.URL,
Branch: ghCfg.Branch,
Path: ghCfg.Path,
Token: ghCfg.Token,
Token: ghToken,
EncryptedToken: ghCfg.EncryptedToken,
}
nanogitRepo, err := nanogit.NewGitRepository(ctx, e.secrets, r, gitCfg)
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg)
if err != nil {
return nil, fmt.Errorf("error creating nanogit repository: %w", err)
return nil, fmt.Errorf("error creating git repository: %w", err)
}
basicRepo := nanogit.NewGithubRepository(apiRepo, nanogitRepo)
basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken)
if err != nil {
return nil, fmt.Errorf("error creating github repository: %w", err)
}
return NewGithubWebhookRepository(basicRepo, webhookURL, e.secrets), nil
}
@@ -25,14 +25,14 @@ type WebhookRepository interface {
}
type GithubWebhookRepository interface {
repository.GithubRepository
pgh.GithubRepository
repository.Hooks
WebhookRepository
}
type githubWebhookRepository struct {
repository.GithubRepository
pgh.GithubRepository
config *provisioning.Repository
owner string
repo string
@@ -42,7 +42,7 @@ type githubWebhookRepository struct {
}
func NewGithubWebhookRepository(
basic repository.GithubRepository,
basic pgh.GithubRepository,
webhookURL string,
secrets secrets.Service,
) GithubWebhookRepository {
@@ -15,7 +15,7 @@ import (
"testing"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -983,14 +983,14 @@ func TestGitHubRepository_Webhook(t *testing.T) {
func TestGitHubRepository_CommentPullRequest(t *testing.T) {
tests := []struct {
name string
setupMock func(m *pgh.MockClient)
setupMock func(m *github.MockClient)
prNumber int
comment string
expectedError error
}{
{
name: "successfully comment on pull request",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
m.On("CreatePullRequestComment", mock.Anything, "grafana", "grafana", 123, "Test comment").
Return(nil)
},
@@ -1000,7 +1000,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) {
},
{
name: "error commenting on pull request",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
m.On("CreatePullRequestComment", mock.Anything, "grafana", "grafana", 456, "Error comment").
Return(fmt.Errorf("failed to create comment"))
},
@@ -1013,7 +1013,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock GitHub client
mockGH := pgh.NewMockClient(t)
mockGH := github.NewMockClient(t)
tt.setupMock(mockGH)
// Create repository with mock
@@ -1050,7 +1050,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) {
func TestGitHubRepository_OnCreate(t *testing.T) {
tests := []struct {
name string
setupMock func(m *pgh.MockClient)
setupMock func(m *github.MockClient)
config *provisioning.Repository
webhookURL string
expectedHook *provisioning.WebhookStatus
@@ -1058,12 +1058,12 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
}{
{
name: "successfully create webhook",
setupMock: func(m *pgh.MockClient) {
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(cfg pgh.WebhookConfig) bool {
setupMock: func(m *github.MockClient) {
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(cfg github.WebhookConfig) bool {
return cfg.URL == "https://example.com/webhook" &&
cfg.ContentType == "json" &&
cfg.Active == true
})).Return(pgh.WebhookConfig{
})).Return(github.WebhookConfig{
ID: 123,
URL: "https://example.com/webhook",
Secret: "test-secret",
@@ -1086,7 +1086,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
},
{
name: "no webhook URL",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// No webhook creation expected
},
config: &provisioning.Repository{
@@ -1102,9 +1102,9 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
},
{
name: "error creating webhook",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
@@ -1122,7 +1122,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock GitHub client
mockGH := pgh.NewMockClient(t)
mockGH := github.NewMockClient(t)
tt.setupMock(mockGH)
// Create repository with mock
@@ -1166,7 +1166,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
func TestGitHubRepository_OnUpdate(t *testing.T) {
tests := []struct {
name string
setupMock func(m *pgh.MockClient)
setupMock func(m *github.MockClient)
config *provisioning.Repository
webhookURL string
expectedHook *provisioning.WebhookStatus
@@ -1174,17 +1174,17 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
}{
{
name: "successfully update webhook when webhook exists",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock getting the existing webhook
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{
Return(github.WebhookConfig{
ID: 123,
URL: "https://example.com/webhook",
Events: []string{"push"},
}, nil)
// Mock editing the webhook
m.On("EditWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool {
m.On("EditWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool {
return hook.ID == 123 && hook.URL == "https://example.com/webhook-updated" &&
slices.Equal(hook.Events, subscribedEvents)
})).Return(nil)
@@ -1212,18 +1212,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "create webhook when it doesn't exist",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock webhook not found
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound)
Return(github.WebhookConfig{}, github.ErrResourceNotFound)
// Mock creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool {
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool {
return hook.URL == "https://example.com/webhook" &&
hook.ContentType == "json" &&
slices.Equal(hook.Events, subscribedEvents) &&
hook.Active == true
})).Return(pgh.WebhookConfig{
})).Return(github.WebhookConfig{
ID: 456,
URL: "https://example.com/webhook",
Events: subscribedEvents,
@@ -1252,7 +1252,7 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "no webhook URL provided",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// No mocks needed
},
config: &provisioning.Repository{},
@@ -1262,9 +1262,9 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "error getting webhook",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{}, fmt.Errorf("failed to get webhook"))
Return(github.WebhookConfig{}, fmt.Errorf("failed to get webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
@@ -1285,10 +1285,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "error editing webhook",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock getting the existing webhook
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{
Return(github.WebhookConfig{
ID: 123,
URL: "https://example.com/webhook",
Events: []string{"push"},
@@ -1317,10 +1317,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "create webhook when webhook status is nil",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(pgh.WebhookConfig{
Return(github.WebhookConfig{
ID: 456,
URL: "https://example.com/webhook",
Events: subscribedEvents,
@@ -1348,10 +1348,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "create webhook when webhook ID is zero",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(pgh.WebhookConfig{
Return(github.WebhookConfig{
ID: 789,
URL: "https://example.com/webhook",
Events: subscribedEvents,
@@ -1382,10 +1382,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "error when creating webhook fails",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock webhook creation failure
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
@@ -1403,18 +1403,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "creates webhook when ErrResourceNotFound",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock webhook not found
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound)
Return(github.WebhookConfig{}, github.ErrResourceNotFound)
// Mock creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool {
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool {
return hook.URL == "https://example.com/webhook" &&
hook.ContentType == "json" &&
slices.Equal(hook.Events, subscribedEvents) &&
hook.Active == true
})).Return(pgh.WebhookConfig{
})).Return(github.WebhookConfig{
ID: 456,
URL: "https://example.com/webhook",
Events: subscribedEvents,
@@ -1443,18 +1443,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "error on create when not found",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock webhook not found
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound)
Return(github.WebhookConfig{}, github.ErrResourceNotFound)
// Mock error when creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool {
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool {
return hook.URL == "https://example.com/webhook" &&
hook.ContentType == "json" &&
slices.Equal(hook.Events, subscribedEvents) &&
hook.Active == true
})).Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
})).Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
@@ -1475,10 +1475,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
},
{
name: "no update needed when URL and events match",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock getting the existing webhook with matching URL and events
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{
Return(github.WebhookConfig{
ID: 123,
URL: "https://example.com/webhook",
Events: subscribedEvents,
@@ -1514,7 +1514,7 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock GitHub client
mockGH := pgh.NewMockClient(t)
mockGH := github.NewMockClient(t)
tt.setupMock(mockGH)
// Create repository with mock
@@ -1563,14 +1563,14 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
func TestGitHubRepository_OnDelete(t *testing.T) {
tests := []struct {
name string
setupMock func(m *pgh.MockClient)
setupMock func(m *github.MockClient)
config *provisioning.Repository
webhookURL string
expectedError error
}{
{
name: "successfully delete webhook",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock deleting the webhook
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(nil)
@@ -1593,7 +1593,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
},
{
name: "no webhook URL provided",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// No mocks needed
},
config: &provisioning.Repository{},
@@ -1602,7 +1602,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
},
{
name: "webhook not found in status",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// No mocks needed
},
config: &provisioning.Repository{
@@ -1620,7 +1620,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
},
{
name: "error deleting webhook",
setupMock: func(m *pgh.MockClient) {
setupMock: func(m *github.MockClient) {
// Mock webhook deletion failure
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(fmt.Errorf("failed to delete webhook"))
@@ -1646,7 +1646,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock GitHub client
mockGH := pgh.NewMockClient(t)
mockGH := github.NewMockClient(t)
tt.setupMock(mockGH)
// Create repository with mock