App Platform Provisioning: Add experimental nanogit mode for Git Sync (#106763)

* Add nanogit package
* Add nanoGit feature flag
* Put logger into nanogit context
* Commit go mod and go sum updates
* Add more stuff around logging
* Nanogit also in extra one
* Add owner to dependency
This commit is contained in:
Roberto Jiménez Sánchez
2025-06-17 19:07:30 +02:00
committed by GitHub
parent 5e9bb9f506
commit 689cafc1fa
32 changed files with 8034 additions and 64 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ require (
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
+2 -2
View File
@@ -121,8 +121,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+1 -1
View File
@@ -34,7 +34,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
+2 -2
View File
@@ -70,8 +70,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
+1 -1
View File
@@ -41,7 +41,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
+2 -2
View File
@@ -72,8 +72,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
+41 -1
View File
@@ -47,6 +47,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"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/resources/signature"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
@@ -167,6 +168,13 @@ func RegisterAPIService(
return nil, nil
}
logger := logging.DefaultLogger.With("logger", "provisioning startup")
if features.IsEnabledGlobally(featuremgmt.FlagNanoGit) {
logger.Info("Using nanogit for repositories")
} else {
logger.Debug("Using go-git and Github API for repositories")
}
folderResolver := &repository.LocalFolderResolver{
PermittedPrefixes: cfg.PermittedProvisioningPaths,
HomePath: safepath.Clean(cfg.HomePath),
@@ -1122,7 +1130,39 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
cloneFn := func(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) {
return gogit.Clone(ctx, b.clonedir, r, opts, b.secrets)
}
return repository.NewGitHub(ctx, r, b.ghFactory, b.secrets, cloneFn)
apiRepo, err := repository.NewGitHub(ctx, r, b.ghFactory, b.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 !b.features.IsEnabledGlobally(featuremgmt.FlagNanoGit) {
logger.Debug("Instantiating Github repository with go-git and Github API")
return apiRepo, nil
}
logger.Info("Instantiating Github repository with nanogit")
ghCfg := r.Spec.GitHub
if ghCfg == nil {
return nil, fmt.Errorf("github configuration is required for nano git")
}
gitCfg := nanogit.RepositoryConfig{
URL: ghCfg.URL,
Branch: ghCfg.Branch,
Path: ghCfg.Path,
Token: ghCfg.Token,
EncryptedToken: ghCfg.EncryptedToken,
}
nanogitRepo, err := nanogit.NewGitRepository(ctx, b.secrets, r, gitCfg)
if err != nil {
return nil, fmt.Errorf("error creating nanogit repository: %w", err)
}
return nanogit.NewGithubRepository(apiRepo, nanogitRepo), nil
default:
return nil, fmt.Errorf("unknown repository type (%s)", r.Spec.Type)
}
@@ -0,0 +1,15 @@
package repository
// GitRepository is an interface that combines all repository capabilities
// needed for Git repositories.
//
//go:generate mockery --name GitRepository --structname MockGitRepository --inpackage --filename git_repository_mock.go --with-expecter
type GitRepository interface {
Repository
Versioned
Writer
Reader
ClonableRepository
URL() string
Branch() string
}
@@ -0,0 +1,834 @@
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository
import (
context "context"
mock "github.com/stretchr/testify/mock"
field "k8s.io/apimachinery/pkg/util/validation/field"
v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
// MockGitRepository is an autogenerated mock type for the GitRepository type
type MockGitRepository struct {
mock.Mock
}
type MockGitRepository_Expecter struct {
mock *mock.Mock
}
func (_m *MockGitRepository) EXPECT() *MockGitRepository_Expecter {
return &MockGitRepository_Expecter{mock: &_m.Mock}
}
// Branch provides a mock function with no fields
func (_m *MockGitRepository) Branch() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Branch")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockGitRepository_Branch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Branch'
type MockGitRepository_Branch_Call struct {
*mock.Call
}
// Branch is a helper method to define mock.On call
func (_e *MockGitRepository_Expecter) Branch() *MockGitRepository_Branch_Call {
return &MockGitRepository_Branch_Call{Call: _e.mock.On("Branch")}
}
func (_c *MockGitRepository_Branch_Call) Run(run func()) *MockGitRepository_Branch_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGitRepository_Branch_Call) Return(_a0 string) *MockGitRepository_Branch_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGitRepository_Branch_Call) RunAndReturn(run func() string) *MockGitRepository_Branch_Call {
_c.Call.Return(run)
return _c
}
// Clone provides a mock function with given fields: ctx, opts
func (_m *MockGitRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) {
ret := _m.Called(ctx, opts)
if len(ret) == 0 {
panic("no return value specified for Clone")
}
var r0 ClonedRepository
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok {
return rf(ctx, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok {
r0 = rf(ctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(ClonedRepository)
}
}
if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok {
r1 = rf(ctx, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone'
type MockGitRepository_Clone_Call struct {
*mock.Call
}
// Clone is a helper method to define mock.On call
// - ctx context.Context
// - opts CloneOptions
func (_e *MockGitRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockGitRepository_Clone_Call {
return &MockGitRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)}
}
func (_c *MockGitRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockGitRepository_Clone_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(CloneOptions))
})
return _c
}
func (_c *MockGitRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockGitRepository_Clone_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockGitRepository_Clone_Call {
_c.Call.Return(run)
return _c
}
// CompareFiles provides a mock function with given fields: ctx, base, ref
func (_m *MockGitRepository) CompareFiles(ctx context.Context, base string, ref string) ([]VersionedFileChange, error) {
ret := _m.Called(ctx, base, ref)
if len(ret) == 0 {
panic("no return value specified for CompareFiles")
}
var r0 []VersionedFileChange
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]VersionedFileChange, error)); ok {
return rf(ctx, base, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) []VersionedFileChange); ok {
r0 = rf(ctx, base, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]VersionedFileChange)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, base, ref)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_CompareFiles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompareFiles'
type MockGitRepository_CompareFiles_Call struct {
*mock.Call
}
// CompareFiles is a helper method to define mock.On call
// - ctx context.Context
// - base string
// - ref string
func (_e *MockGitRepository_Expecter) CompareFiles(ctx interface{}, base interface{}, ref interface{}) *MockGitRepository_CompareFiles_Call {
return &MockGitRepository_CompareFiles_Call{Call: _e.mock.On("CompareFiles", ctx, base, ref)}
}
func (_c *MockGitRepository_CompareFiles_Call) Run(run func(ctx context.Context, base string, ref string)) *MockGitRepository_CompareFiles_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockGitRepository_CompareFiles_Call) Return(_a0 []VersionedFileChange, _a1 error) *MockGitRepository_CompareFiles_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]VersionedFileChange, error)) *MockGitRepository_CompareFiles_Call {
_c.Call.Return(run)
return _c
}
// Config provides a mock function with no fields
func (_m *MockGitRepository) Config() *v0alpha1.Repository {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Config")
}
var r0 *v0alpha1.Repository
if rf, ok := ret.Get(0).(func() *v0alpha1.Repository); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0alpha1.Repository)
}
}
return r0
}
// MockGitRepository_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config'
type MockGitRepository_Config_Call struct {
*mock.Call
}
// Config is a helper method to define mock.On call
func (_e *MockGitRepository_Expecter) Config() *MockGitRepository_Config_Call {
return &MockGitRepository_Config_Call{Call: _e.mock.On("Config")}
}
func (_c *MockGitRepository_Config_Call) Run(run func()) *MockGitRepository_Config_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGitRepository_Config_Call) Return(_a0 *v0alpha1.Repository) *MockGitRepository_Config_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGitRepository_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockGitRepository_Config_Call {
_c.Call.Return(run)
return _c
}
// Create provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockGitRepository) Create(ctx context.Context, path string, ref string, data []byte, message string) error {
ret := _m.Called(ctx, path, ref, data, message)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok {
r0 = rf(ctx, path, ref, data, message)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGitRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
type MockGitRepository_Create_Call struct {
*mock.Call
}
// Create is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
// - data []byte
// - message string
func (_e *MockGitRepository_Expecter) Create(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockGitRepository_Create_Call {
return &MockGitRepository_Create_Call{Call: _e.mock.On("Create", ctx, path, ref, data, message)}
}
func (_c *MockGitRepository_Create_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockGitRepository_Create_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string))
})
return _c
}
func (_c *MockGitRepository_Create_Call) Return(_a0 error) *MockGitRepository_Create_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGitRepository_Create_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockGitRepository_Create_Call {
_c.Call.Return(run)
return _c
}
// Delete provides a mock function with given fields: ctx, path, ref, message
func (_m *MockGitRepository) Delete(ctx context.Context, path string, ref string, message string) error {
ret := _m.Called(ctx, path, ref, message)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
r0 = rf(ctx, path, ref, message)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGitRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type MockGitRepository_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
// - message string
func (_e *MockGitRepository_Expecter) Delete(ctx interface{}, path interface{}, ref interface{}, message interface{}) *MockGitRepository_Delete_Call {
return &MockGitRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, path, ref, message)}
}
func (_c *MockGitRepository_Delete_Call) Run(run func(ctx context.Context, path string, ref string, message string)) *MockGitRepository_Delete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string))
})
return _c
}
func (_c *MockGitRepository_Delete_Call) Return(_a0 error) *MockGitRepository_Delete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGitRepository_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockGitRepository_Delete_Call {
_c.Call.Return(run)
return _c
}
// History provides a mock function with given fields: ctx, path, ref
func (_m *MockGitRepository) History(ctx context.Context, path string, ref string) ([]v0alpha1.HistoryItem, error) {
ret := _m.Called(ctx, path, ref)
if len(ret) == 0 {
panic("no return value specified for History")
}
var r0 []v0alpha1.HistoryItem
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]v0alpha1.HistoryItem, error)); ok {
return rf(ctx, path, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) []v0alpha1.HistoryItem); ok {
r0 = rf(ctx, path, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]v0alpha1.HistoryItem)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, path, ref)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_History_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'History'
type MockGitRepository_History_Call struct {
*mock.Call
}
// History is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
func (_e *MockGitRepository_Expecter) History(ctx interface{}, path interface{}, ref interface{}) *MockGitRepository_History_Call {
return &MockGitRepository_History_Call{Call: _e.mock.On("History", ctx, path, ref)}
}
func (_c *MockGitRepository_History_Call) Run(run func(ctx context.Context, path string, ref string)) *MockGitRepository_History_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockGitRepository_History_Call) Return(_a0 []v0alpha1.HistoryItem, _a1 error) *MockGitRepository_History_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_History_Call) RunAndReturn(run func(context.Context, string, string) ([]v0alpha1.HistoryItem, error)) *MockGitRepository_History_Call {
_c.Call.Return(run)
return _c
}
// LatestRef provides a mock function with given fields: ctx
func (_m *MockGitRepository) LatestRef(ctx context.Context) (string, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for LatestRef")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) string); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_LatestRef_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestRef'
type MockGitRepository_LatestRef_Call struct {
*mock.Call
}
// LatestRef is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockGitRepository_Expecter) LatestRef(ctx interface{}) *MockGitRepository_LatestRef_Call {
return &MockGitRepository_LatestRef_Call{Call: _e.mock.On("LatestRef", ctx)}
}
func (_c *MockGitRepository_LatestRef_Call) Run(run func(ctx context.Context)) *MockGitRepository_LatestRef_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockGitRepository_LatestRef_Call) Return(_a0 string, _a1 error) *MockGitRepository_LatestRef_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_LatestRef_Call) RunAndReturn(run func(context.Context) (string, error)) *MockGitRepository_LatestRef_Call {
_c.Call.Return(run)
return _c
}
// Read provides a mock function with given fields: ctx, path, ref
func (_m *MockGitRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) {
ret := _m.Called(ctx, path, ref)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 *FileInfo
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok {
return rf(ctx, path, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok {
r0 = rf(ctx, path, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*FileInfo)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, path, ref)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
type MockGitRepository_Read_Call struct {
*mock.Call
}
// Read is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
func (_e *MockGitRepository_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockGitRepository_Read_Call {
return &MockGitRepository_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)}
}
func (_c *MockGitRepository_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockGitRepository_Read_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockGitRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockGitRepository_Read_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockGitRepository_Read_Call {
_c.Call.Return(run)
return _c
}
// ReadTree provides a mock function with given fields: ctx, ref
func (_m *MockGitRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) {
ret := _m.Called(ctx, ref)
if len(ret) == 0 {
panic("no return value specified for ReadTree")
}
var r0 []FileTreeEntry
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok {
return rf(ctx, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok {
r0 = rf(ctx, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]FileTreeEntry)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, ref)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_ReadTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadTree'
type MockGitRepository_ReadTree_Call struct {
*mock.Call
}
// ReadTree is a helper method to define mock.On call
// - ctx context.Context
// - ref string
func (_e *MockGitRepository_Expecter) ReadTree(ctx interface{}, ref interface{}) *MockGitRepository_ReadTree_Call {
return &MockGitRepository_ReadTree_Call{Call: _e.mock.On("ReadTree", ctx, ref)}
}
func (_c *MockGitRepository_ReadTree_Call) Run(run func(ctx context.Context, ref string)) *MockGitRepository_ReadTree_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *MockGitRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockGitRepository_ReadTree_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockGitRepository_ReadTree_Call {
_c.Call.Return(run)
return _c
}
// Test provides a mock function with given fields: ctx
func (_m *MockGitRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Test")
}
var r0 *v0alpha1.TestResults
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (*v0alpha1.TestResults, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) *v0alpha1.TestResults); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0alpha1.TestResults)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_Test_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Test'
type MockGitRepository_Test_Call struct {
*mock.Call
}
// Test is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockGitRepository_Expecter) Test(ctx interface{}) *MockGitRepository_Test_Call {
return &MockGitRepository_Test_Call{Call: _e.mock.On("Test", ctx)}
}
func (_c *MockGitRepository_Test_Call) Run(run func(ctx context.Context)) *MockGitRepository_Test_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockGitRepository_Test_Call) Return(_a0 *v0alpha1.TestResults, _a1 error) *MockGitRepository_Test_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_Test_Call) RunAndReturn(run func(context.Context) (*v0alpha1.TestResults, error)) *MockGitRepository_Test_Call {
_c.Call.Return(run)
return _c
}
// URL provides a mock function with no fields
func (_m *MockGitRepository) URL() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for URL")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockGitRepository_URL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'URL'
type MockGitRepository_URL_Call struct {
*mock.Call
}
// URL is a helper method to define mock.On call
func (_e *MockGitRepository_Expecter) URL() *MockGitRepository_URL_Call {
return &MockGitRepository_URL_Call{Call: _e.mock.On("URL")}
}
func (_c *MockGitRepository_URL_Call) Run(run func()) *MockGitRepository_URL_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGitRepository_URL_Call) Return(_a0 string) *MockGitRepository_URL_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGitRepository_URL_Call) RunAndReturn(run func() string) *MockGitRepository_URL_Call {
_c.Call.Return(run)
return _c
}
// Update provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockGitRepository) Update(ctx context.Context, path string, ref string, data []byte, message string) error {
ret := _m.Called(ctx, path, ref, data, message)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok {
r0 = rf(ctx, path, ref, data, message)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGitRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
type MockGitRepository_Update_Call struct {
*mock.Call
}
// Update is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
// - data []byte
// - message string
func (_e *MockGitRepository_Expecter) Update(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockGitRepository_Update_Call {
return &MockGitRepository_Update_Call{Call: _e.mock.On("Update", ctx, path, ref, data, message)}
}
func (_c *MockGitRepository_Update_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockGitRepository_Update_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string))
})
return _c
}
func (_c *MockGitRepository_Update_Call) Return(_a0 error) *MockGitRepository_Update_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGitRepository_Update_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockGitRepository_Update_Call {
_c.Call.Return(run)
return _c
}
// Validate provides a mock function with no fields
func (_m *MockGitRepository) Validate() field.ErrorList {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Validate")
}
var r0 field.ErrorList
if rf, ok := ret.Get(0).(func() field.ErrorList); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(field.ErrorList)
}
}
return r0
}
// MockGitRepository_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
type MockGitRepository_Validate_Call struct {
*mock.Call
}
// Validate is a helper method to define mock.On call
func (_e *MockGitRepository_Expecter) Validate() *MockGitRepository_Validate_Call {
return &MockGitRepository_Validate_Call{Call: _e.mock.On("Validate")}
}
func (_c *MockGitRepository_Validate_Call) Run(run func()) *MockGitRepository_Validate_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGitRepository_Validate_Call) Return(_a0 field.ErrorList) *MockGitRepository_Validate_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGitRepository_Validate_Call) RunAndReturn(run func() field.ErrorList) *MockGitRepository_Validate_Call {
_c.Call.Return(run)
return _c
}
// Write provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockGitRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error {
ret := _m.Called(ctx, path, ref, data, message)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok {
r0 = rf(ctx, path, ref, data, message)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGitRepository_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockGitRepository_Write_Call struct {
*mock.Call
}
// Write is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
// - data []byte
// - message string
func (_e *MockGitRepository_Expecter) Write(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockGitRepository_Write_Call {
return &MockGitRepository_Write_Call{Call: _e.mock.On("Write", ctx, path, ref, data, message)}
}
func (_c *MockGitRepository_Write_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockGitRepository_Write_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string))
})
return _c
}
func (_c *MockGitRepository_Write_Call) Return(_a0 error) *MockGitRepository_Write_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGitRepository_Write_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockGitRepository_Write_Call {
_c.Call.Return(run)
return _c
}
// NewMockGitRepository creates a new instance of MockGitRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockGitRepository(t interface {
mock.TestingT
Cleanup(func())
}) *MockGitRepository {
mock := &MockGitRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
@@ -35,6 +35,8 @@ type githubRepository struct {
// 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
Versioned
@@ -54,7 +56,7 @@ func NewGitHub(
secrets secrets.Service,
cloneFn CloneFn,
) (GithubRepository, error) {
owner, repo, err := parseOwnerRepo(config.Spec.GitHub.URL)
owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL)
if err != nil {
return nil, fmt.Errorf("parse owner and repo: %w", err)
}
@@ -104,7 +106,7 @@ func (r *githubRepository) Validate() (list field.ErrorList) {
if gh.URL == "" {
list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required"))
} else {
_, _, err := parseOwnerRepo(gh.URL)
_, _, 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/") {
@@ -113,7 +115,7 @@ func (r *githubRepository) Validate() (list field.ErrorList) {
}
if gh.Branch == "" {
list = append(list, field.Required(field.NewPath("spec", "github", "branch"), "a github branch is required"))
} else if !isValidGitBranchName(gh.Branch) {
} else if !IsValidGitBranchName(gh.Branch) {
list = append(list, field.Invalid(field.NewPath("spec", "github", "branch"), gh.Branch, "invalid branch name"))
}
// TODO: Use two fields for token
@@ -132,7 +134,7 @@ func (r *githubRepository) Validate() (list field.ErrorList) {
return list
}
func parseOwnerRepo(giturl string) (owner string, repo string, err error) {
func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) {
parsed, e := url.Parse(strings.TrimSuffix(giturl, ".git"))
if e != nil {
err = e
@@ -160,7 +162,7 @@ func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults,
}
url := r.config.Spec.GitHub.URL
owner, repo, err := parseOwnerRepo(url)
owner, repo, err := ParseOwnerRepoGithub(url)
if err != nil {
return fromFieldError(field.Invalid(
field.NewPath("spec", "github", "url"), url, err.Error())), nil
@@ -459,7 +461,7 @@ func (r *githubRepository) History(ctx context.Context, path, ref string) ([]pro
// it does not cover all cases as positive lookaheads are not supported in Go's regexp
var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`)
// isValidGitBranchName checks if a branch name is valid.
// IsValidGitBranchName checks if a branch name is valid.
// It uses the following regexp `^[a-zA-Z0-9\-\_\/\.]+$` to validate the branch name with some additional checks that must satisfy the following rules:
// 1. The branch name must have at least one character and must not be empty.
// 2. The branch name cannot start with `/` or end with `/`, `.`, or whitespace.
@@ -467,7 +469,7 @@ var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`)
// 4. The branch name cannot contain consecutive dots (`..`).
// 5. The branch name cannot contain `@{`.
// 6. The branch name cannot include the following characters: `~`, `^`, `:`, `?`, `*`, `[`, `\`, or `]`.
func isValidGitBranchName(branch string) bool {
func IsValidGitBranchName(branch string) bool {
if !basicGitBranchNameRegex.MatchString(branch) {
return false
}
@@ -483,7 +485,7 @@ func isValidGitBranchName(branch string) bool {
}
func (r *githubRepository) ensureBranchExists(ctx context.Context, branchName string) error {
if !isValidGitBranchName(branchName) {
if !IsValidGitBranchName(branchName) {
return &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusBadRequest,
@@ -0,0 +1,942 @@
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository
import (
context "context"
github "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
field "k8s.io/apimachinery/pkg/util/validation/field"
mock "github.com/stretchr/testify/mock"
v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
// MockGithubRepository is an autogenerated mock type for the GithubRepository type
type MockGithubRepository struct {
mock.Mock
}
type MockGithubRepository_Expecter struct {
mock *mock.Mock
}
func (_m *MockGithubRepository) EXPECT() *MockGithubRepository_Expecter {
return &MockGithubRepository_Expecter{mock: &_m.Mock}
}
// Client provides a mock function with no fields
func (_m *MockGithubRepository) Client() github.Client {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Client")
}
var r0 github.Client
if rf, ok := ret.Get(0).(func() github.Client); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(github.Client)
}
}
return r0
}
// MockGithubRepository_Client_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Client'
type MockGithubRepository_Client_Call struct {
*mock.Call
}
// Client is a helper method to define mock.On call
func (_e *MockGithubRepository_Expecter) Client() *MockGithubRepository_Client_Call {
return &MockGithubRepository_Client_Call{Call: _e.mock.On("Client")}
}
func (_c *MockGithubRepository_Client_Call) Run(run func()) *MockGithubRepository_Client_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGithubRepository_Client_Call) Return(_a0 github.Client) *MockGithubRepository_Client_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Client_Call) RunAndReturn(run func() github.Client) *MockGithubRepository_Client_Call {
_c.Call.Return(run)
return _c
}
// Clone provides a mock function with given fields: ctx, opts
func (_m *MockGithubRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) {
ret := _m.Called(ctx, opts)
if len(ret) == 0 {
panic("no return value specified for Clone")
}
var r0 ClonedRepository
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok {
return rf(ctx, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok {
r0 = rf(ctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(ClonedRepository)
}
}
if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok {
r1 = rf(ctx, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone'
type MockGithubRepository_Clone_Call struct {
*mock.Call
}
// Clone is a helper method to define mock.On call
// - ctx context.Context
// - opts CloneOptions
func (_e *MockGithubRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockGithubRepository_Clone_Call {
return &MockGithubRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)}
}
func (_c *MockGithubRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockGithubRepository_Clone_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(CloneOptions))
})
return _c
}
func (_c *MockGithubRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockGithubRepository_Clone_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockGithubRepository_Clone_Call {
_c.Call.Return(run)
return _c
}
// CompareFiles provides a mock function with given fields: ctx, base, ref
func (_m *MockGithubRepository) CompareFiles(ctx context.Context, base string, ref string) ([]VersionedFileChange, error) {
ret := _m.Called(ctx, base, ref)
if len(ret) == 0 {
panic("no return value specified for CompareFiles")
}
var r0 []VersionedFileChange
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]VersionedFileChange, error)); ok {
return rf(ctx, base, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) []VersionedFileChange); ok {
r0 = rf(ctx, base, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]VersionedFileChange)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, base, ref)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_CompareFiles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompareFiles'
type MockGithubRepository_CompareFiles_Call struct {
*mock.Call
}
// CompareFiles is a helper method to define mock.On call
// - ctx context.Context
// - base string
// - ref string
func (_e *MockGithubRepository_Expecter) CompareFiles(ctx interface{}, base interface{}, ref interface{}) *MockGithubRepository_CompareFiles_Call {
return &MockGithubRepository_CompareFiles_Call{Call: _e.mock.On("CompareFiles", ctx, base, ref)}
}
func (_c *MockGithubRepository_CompareFiles_Call) Run(run func(ctx context.Context, base string, ref string)) *MockGithubRepository_CompareFiles_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockGithubRepository_CompareFiles_Call) Return(_a0 []VersionedFileChange, _a1 error) *MockGithubRepository_CompareFiles_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]VersionedFileChange, error)) *MockGithubRepository_CompareFiles_Call {
_c.Call.Return(run)
return _c
}
// Config provides a mock function with no fields
func (_m *MockGithubRepository) Config() *v0alpha1.Repository {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Config")
}
var r0 *v0alpha1.Repository
if rf, ok := ret.Get(0).(func() *v0alpha1.Repository); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0alpha1.Repository)
}
}
return r0
}
// MockGithubRepository_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config'
type MockGithubRepository_Config_Call struct {
*mock.Call
}
// Config is a helper method to define mock.On call
func (_e *MockGithubRepository_Expecter) Config() *MockGithubRepository_Config_Call {
return &MockGithubRepository_Config_Call{Call: _e.mock.On("Config")}
}
func (_c *MockGithubRepository_Config_Call) Run(run func()) *MockGithubRepository_Config_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGithubRepository_Config_Call) Return(_a0 *v0alpha1.Repository) *MockGithubRepository_Config_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockGithubRepository_Config_Call {
_c.Call.Return(run)
return _c
}
// Create provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockGithubRepository) Create(ctx context.Context, path string, ref string, data []byte, message string) error {
ret := _m.Called(ctx, path, ref, data, message)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok {
r0 = rf(ctx, path, ref, data, message)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGithubRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
type MockGithubRepository_Create_Call struct {
*mock.Call
}
// Create is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
// - data []byte
// - message string
func (_e *MockGithubRepository_Expecter) Create(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockGithubRepository_Create_Call {
return &MockGithubRepository_Create_Call{Call: _e.mock.On("Create", ctx, path, ref, data, message)}
}
func (_c *MockGithubRepository_Create_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockGithubRepository_Create_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string))
})
return _c
}
func (_c *MockGithubRepository_Create_Call) Return(_a0 error) *MockGithubRepository_Create_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Create_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockGithubRepository_Create_Call {
_c.Call.Return(run)
return _c
}
// Delete provides a mock function with given fields: ctx, path, ref, message
func (_m *MockGithubRepository) Delete(ctx context.Context, path string, ref string, message string) error {
ret := _m.Called(ctx, path, ref, message)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
r0 = rf(ctx, path, ref, message)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGithubRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type MockGithubRepository_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
// - message string
func (_e *MockGithubRepository_Expecter) Delete(ctx interface{}, path interface{}, ref interface{}, message interface{}) *MockGithubRepository_Delete_Call {
return &MockGithubRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, path, ref, message)}
}
func (_c *MockGithubRepository_Delete_Call) Run(run func(ctx context.Context, path string, ref string, message string)) *MockGithubRepository_Delete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string))
})
return _c
}
func (_c *MockGithubRepository_Delete_Call) Return(_a0 error) *MockGithubRepository_Delete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockGithubRepository_Delete_Call {
_c.Call.Return(run)
return _c
}
// History provides a mock function with given fields: ctx, path, ref
func (_m *MockGithubRepository) History(ctx context.Context, path string, ref string) ([]v0alpha1.HistoryItem, error) {
ret := _m.Called(ctx, path, ref)
if len(ret) == 0 {
panic("no return value specified for History")
}
var r0 []v0alpha1.HistoryItem
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]v0alpha1.HistoryItem, error)); ok {
return rf(ctx, path, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) []v0alpha1.HistoryItem); ok {
r0 = rf(ctx, path, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]v0alpha1.HistoryItem)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, path, ref)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_History_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'History'
type MockGithubRepository_History_Call struct {
*mock.Call
}
// History is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
func (_e *MockGithubRepository_Expecter) History(ctx interface{}, path interface{}, ref interface{}) *MockGithubRepository_History_Call {
return &MockGithubRepository_History_Call{Call: _e.mock.On("History", ctx, path, ref)}
}
func (_c *MockGithubRepository_History_Call) Run(run func(ctx context.Context, path string, ref string)) *MockGithubRepository_History_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockGithubRepository_History_Call) Return(_a0 []v0alpha1.HistoryItem, _a1 error) *MockGithubRepository_History_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_History_Call) RunAndReturn(run func(context.Context, string, string) ([]v0alpha1.HistoryItem, error)) *MockGithubRepository_History_Call {
_c.Call.Return(run)
return _c
}
// LatestRef provides a mock function with given fields: ctx
func (_m *MockGithubRepository) LatestRef(ctx context.Context) (string, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for LatestRef")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) string); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_LatestRef_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestRef'
type MockGithubRepository_LatestRef_Call struct {
*mock.Call
}
// LatestRef is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockGithubRepository_Expecter) LatestRef(ctx interface{}) *MockGithubRepository_LatestRef_Call {
return &MockGithubRepository_LatestRef_Call{Call: _e.mock.On("LatestRef", ctx)}
}
func (_c *MockGithubRepository_LatestRef_Call) Run(run func(ctx context.Context)) *MockGithubRepository_LatestRef_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockGithubRepository_LatestRef_Call) Return(_a0 string, _a1 error) *MockGithubRepository_LatestRef_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_LatestRef_Call) RunAndReturn(run func(context.Context) (string, error)) *MockGithubRepository_LatestRef_Call {
_c.Call.Return(run)
return _c
}
// Owner provides a mock function with no fields
func (_m *MockGithubRepository) Owner() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Owner")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockGithubRepository_Owner_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Owner'
type MockGithubRepository_Owner_Call struct {
*mock.Call
}
// Owner is a helper method to define mock.On call
func (_e *MockGithubRepository_Expecter) Owner() *MockGithubRepository_Owner_Call {
return &MockGithubRepository_Owner_Call{Call: _e.mock.On("Owner")}
}
func (_c *MockGithubRepository_Owner_Call) Run(run func()) *MockGithubRepository_Owner_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGithubRepository_Owner_Call) Return(_a0 string) *MockGithubRepository_Owner_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Owner_Call) RunAndReturn(run func() string) *MockGithubRepository_Owner_Call {
_c.Call.Return(run)
return _c
}
// Read provides a mock function with given fields: ctx, path, ref
func (_m *MockGithubRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) {
ret := _m.Called(ctx, path, ref)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 *FileInfo
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok {
return rf(ctx, path, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok {
r0 = rf(ctx, path, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*FileInfo)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, path, ref)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
type MockGithubRepository_Read_Call struct {
*mock.Call
}
// Read is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
func (_e *MockGithubRepository_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockGithubRepository_Read_Call {
return &MockGithubRepository_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)}
}
func (_c *MockGithubRepository_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockGithubRepository_Read_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockGithubRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockGithubRepository_Read_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockGithubRepository_Read_Call {
_c.Call.Return(run)
return _c
}
// ReadTree provides a mock function with given fields: ctx, ref
func (_m *MockGithubRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) {
ret := _m.Called(ctx, ref)
if len(ret) == 0 {
panic("no return value specified for ReadTree")
}
var r0 []FileTreeEntry
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok {
return rf(ctx, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok {
r0 = rf(ctx, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]FileTreeEntry)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, ref)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_ReadTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadTree'
type MockGithubRepository_ReadTree_Call struct {
*mock.Call
}
// ReadTree is a helper method to define mock.On call
// - ctx context.Context
// - ref string
func (_e *MockGithubRepository_Expecter) ReadTree(ctx interface{}, ref interface{}) *MockGithubRepository_ReadTree_Call {
return &MockGithubRepository_ReadTree_Call{Call: _e.mock.On("ReadTree", ctx, ref)}
}
func (_c *MockGithubRepository_ReadTree_Call) Run(run func(ctx context.Context, ref string)) *MockGithubRepository_ReadTree_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *MockGithubRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockGithubRepository_ReadTree_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockGithubRepository_ReadTree_Call {
_c.Call.Return(run)
return _c
}
// Repo provides a mock function with no fields
func (_m *MockGithubRepository) Repo() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Repo")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockGithubRepository_Repo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Repo'
type MockGithubRepository_Repo_Call struct {
*mock.Call
}
// Repo is a helper method to define mock.On call
func (_e *MockGithubRepository_Expecter) Repo() *MockGithubRepository_Repo_Call {
return &MockGithubRepository_Repo_Call{Call: _e.mock.On("Repo")}
}
func (_c *MockGithubRepository_Repo_Call) Run(run func()) *MockGithubRepository_Repo_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGithubRepository_Repo_Call) Return(_a0 string) *MockGithubRepository_Repo_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Repo_Call) RunAndReturn(run func() string) *MockGithubRepository_Repo_Call {
_c.Call.Return(run)
return _c
}
// ResourceURLs provides a mock function with given fields: ctx, file
func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*v0alpha1.ResourceURLs, error) {
ret := _m.Called(ctx, file)
if len(ret) == 0 {
panic("no return value specified for ResourceURLs")
}
var r0 *v0alpha1.ResourceURLs
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *FileInfo) (*v0alpha1.ResourceURLs, error)); ok {
return rf(ctx, file)
}
if rf, ok := ret.Get(0).(func(context.Context, *FileInfo) *v0alpha1.ResourceURLs); ok {
r0 = rf(ctx, file)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0alpha1.ResourceURLs)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *FileInfo) error); ok {
r1 = rf(ctx, file)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_ResourceURLs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResourceURLs'
type MockGithubRepository_ResourceURLs_Call struct {
*mock.Call
}
// ResourceURLs is a helper method to define mock.On call
// - ctx context.Context
// - file *FileInfo
func (_e *MockGithubRepository_Expecter) ResourceURLs(ctx interface{}, file interface{}) *MockGithubRepository_ResourceURLs_Call {
return &MockGithubRepository_ResourceURLs_Call{Call: _e.mock.On("ResourceURLs", ctx, file)}
}
func (_c *MockGithubRepository_ResourceURLs_Call) Run(run func(ctx context.Context, file *FileInfo)) *MockGithubRepository_ResourceURLs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*FileInfo))
})
return _c
}
func (_c *MockGithubRepository_ResourceURLs_Call) Return(_a0 *v0alpha1.ResourceURLs, _a1 error) *MockGithubRepository_ResourceURLs_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_ResourceURLs_Call) RunAndReturn(run func(context.Context, *FileInfo) (*v0alpha1.ResourceURLs, error)) *MockGithubRepository_ResourceURLs_Call {
_c.Call.Return(run)
return _c
}
// Test provides a mock function with given fields: ctx
func (_m *MockGithubRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Test")
}
var r0 *v0alpha1.TestResults
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (*v0alpha1.TestResults, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) *v0alpha1.TestResults); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0alpha1.TestResults)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_Test_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Test'
type MockGithubRepository_Test_Call struct {
*mock.Call
}
// Test is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockGithubRepository_Expecter) Test(ctx interface{}) *MockGithubRepository_Test_Call {
return &MockGithubRepository_Test_Call{Call: _e.mock.On("Test", ctx)}
}
func (_c *MockGithubRepository_Test_Call) Run(run func(ctx context.Context)) *MockGithubRepository_Test_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockGithubRepository_Test_Call) Return(_a0 *v0alpha1.TestResults, _a1 error) *MockGithubRepository_Test_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_Test_Call) RunAndReturn(run func(context.Context) (*v0alpha1.TestResults, error)) *MockGithubRepository_Test_Call {
_c.Call.Return(run)
return _c
}
// Update provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockGithubRepository) Update(ctx context.Context, path string, ref string, data []byte, message string) error {
ret := _m.Called(ctx, path, ref, data, message)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok {
r0 = rf(ctx, path, ref, data, message)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGithubRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
type MockGithubRepository_Update_Call struct {
*mock.Call
}
// Update is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
// - data []byte
// - message string
func (_e *MockGithubRepository_Expecter) Update(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockGithubRepository_Update_Call {
return &MockGithubRepository_Update_Call{Call: _e.mock.On("Update", ctx, path, ref, data, message)}
}
func (_c *MockGithubRepository_Update_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockGithubRepository_Update_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string))
})
return _c
}
func (_c *MockGithubRepository_Update_Call) Return(_a0 error) *MockGithubRepository_Update_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Update_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockGithubRepository_Update_Call {
_c.Call.Return(run)
return _c
}
// Validate provides a mock function with no fields
func (_m *MockGithubRepository) Validate() field.ErrorList {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Validate")
}
var r0 field.ErrorList
if rf, ok := ret.Get(0).(func() field.ErrorList); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(field.ErrorList)
}
}
return r0
}
// MockGithubRepository_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
type MockGithubRepository_Validate_Call struct {
*mock.Call
}
// Validate is a helper method to define mock.On call
func (_e *MockGithubRepository_Expecter) Validate() *MockGithubRepository_Validate_Call {
return &MockGithubRepository_Validate_Call{Call: _e.mock.On("Validate")}
}
func (_c *MockGithubRepository_Validate_Call) Run(run func()) *MockGithubRepository_Validate_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGithubRepository_Validate_Call) Return(_a0 field.ErrorList) *MockGithubRepository_Validate_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Validate_Call) RunAndReturn(run func() field.ErrorList) *MockGithubRepository_Validate_Call {
_c.Call.Return(run)
return _c
}
// Write provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockGithubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error {
ret := _m.Called(ctx, path, ref, data, message)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, string) error); ok {
r0 = rf(ctx, path, ref, data, message)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGithubRepository_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockGithubRepository_Write_Call struct {
*mock.Call
}
// Write is a helper method to define mock.On call
// - ctx context.Context
// - path string
// - ref string
// - data []byte
// - message string
func (_e *MockGithubRepository_Expecter) Write(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockGithubRepository_Write_Call {
return &MockGithubRepository_Write_Call{Call: _e.mock.On("Write", ctx, path, ref, data, message)}
}
func (_c *MockGithubRepository_Write_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockGithubRepository_Write_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string))
})
return _c
}
func (_c *MockGithubRepository_Write_Call) Return(_a0 error) *MockGithubRepository_Write_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGithubRepository_Write_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockGithubRepository_Write_Call {
_c.Call.Return(run)
return _c
}
// NewMockGithubRepository creates a new instance of MockGithubRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockGithubRepository(t interface {
mock.TestingT
Cleanup(func())
}) *MockGithubRepository {
mock := &MockGithubRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
@@ -212,7 +212,7 @@ func TestIsValidGitBranchName(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, isValidGitBranchName(tt.branch))
assert.Equal(t, tt.expected, IsValidGitBranchName(tt.branch))
})
}
}
@@ -661,7 +661,7 @@ func TestGitHubRepository_Test(t *testing.T) {
// If the config has a different URL, parse and set the owner/repo
if tt.config.Spec.GitHub.URL != "https://github.com/grafana/grafana" {
owner, githubRepo, _ := parseOwnerRepo(tt.config.Spec.GitHub.URL)
owner, githubRepo, _ := ParseOwnerRepoGithub(tt.config.Spec.GitHub.URL)
repo.owner = owner
repo.repo = githubRepo
}
@@ -0,0 +1,728 @@
package nanogit
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"github.com/grafana/grafana-app-sdk/logging"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
"github.com/grafana/nanogit"
"github.com/grafana/nanogit/log"
"github.com/grafana/nanogit/options"
"github.com/grafana/nanogit/protocol"
"github.com/grafana/nanogit/protocol/hash"
)
type RepositoryConfig struct {
URL string
Branch string
Token string
EncryptedToken []byte
Path string
}
// Make sure all public functions of this struct call the (*gitRepository).logger function, to ensure the Git repo details are included.
type gitRepository struct {
config *provisioning.Repository
gitConfig RepositoryConfig
client nanogit.Client
secrets secrets.Service
}
func NewGitRepository(
ctx context.Context,
secrets secrets.Service,
config *provisioning.Repository,
gitConfig RepositoryConfig,
) (repository.GitRepository, error) {
if gitConfig.Token == "" {
decrypted, err := secrets.Decrypt(ctx, gitConfig.EncryptedToken)
if err != nil {
return nil, fmt.Errorf("decrypt token: %w", err)
}
gitConfig.Token = string(decrypted)
}
// Create nanogit client with authentication
client, err := nanogit.NewHTTPClient(
gitConfig.URL,
options.WithBasicAuth("git", gitConfig.Token),
)
if err != nil {
return nil, fmt.Errorf("create nanogit client: %w", err)
}
return &gitRepository{
config: config,
gitConfig: gitConfig,
client: client,
secrets: secrets,
}, nil
}
func (r *gitRepository) URL() string {
return r.gitConfig.URL
}
func (r *gitRepository) Branch() string {
return r.gitConfig.Branch
}
func (r *gitRepository) Config() *provisioning.Repository {
return r.config
}
// Validate implements provisioning.Repository.
func (r *gitRepository) Validate() (list field.ErrorList) {
cfg := r.gitConfig
t := string(r.config.Spec.Type)
if cfg.URL == "" {
list = append(list, field.Required(field.NewPath("spec", t, "url"), "a git url is required"))
} else {
if !isValidGitURL(cfg.URL) {
list = append(list, field.Invalid(field.NewPath("spec", t, "url"), cfg.URL, "invalid git URL format"))
}
}
if cfg.Branch == "" {
list = append(list, field.Required(field.NewPath("spec", t, "branch"), "a git branch is required"))
} else if !repository.IsValidGitBranchName(cfg.Branch) {
list = append(list, field.Invalid(field.NewPath("spec", t, "branch"), cfg.Branch, "invalid branch name"))
}
if cfg.Token == "" && len(cfg.EncryptedToken) == 0 {
list = append(list, field.Required(field.NewPath("spec", t, "token"), "a git access token is required"))
}
if err := safepath.IsSafe(cfg.Path); err != nil {
list = append(list, field.Invalid(field.NewPath("spec", t, "path"), cfg.Path, err.Error()))
}
if safepath.IsAbs(cfg.Path) {
list = append(list, field.Invalid(field.NewPath("spec", t, "path"), cfg.Path, "path must be relative"))
}
return list
}
func isValidGitURL(gitURL string) bool {
// Parse URL
parsed, err := url.Parse(gitURL)
if err != nil {
return false
}
// Must be HTTPS
if parsed.Scheme != "https" {
return false
}
// Must have a host
if parsed.Host == "" {
return false
}
// Must have a path
if parsed.Path == "" || parsed.Path == "/" {
return false
}
return true
}
// Test implements provisioning.Repository.
func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, error) {
ctx, _ = r.logger(ctx, "")
t := string(r.config.Spec.Type)
if ok, err := r.client.IsAuthorized(ctx); err != nil || !ok {
detail := "not authorized"
if err != nil {
detail = fmt.Sprintf("failed check if authorized: %v", err)
}
return &provisioning.TestResults{
Code: http.StatusBadRequest,
Success: false,
Errors: []provisioning.ErrorDetails{{
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", t, "token").String(),
Detail: detail,
}}}, nil
}
if ok, err := r.client.RepoExists(ctx); err != nil || !ok {
detail := "repository not found"
if err != nil {
detail = fmt.Sprintf("failed check if repository exists: %v", err)
}
return &provisioning.TestResults{
Code: http.StatusBadRequest,
Success: false,
Errors: []provisioning.ErrorDetails{{
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", t, "url").String(),
Detail: detail,
}}}, nil
}
// Test basic connectivity by getting the branch reference
_, err := r.client.GetRef(ctx, fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch))
if err != nil {
detail := "branch not found"
if errors.Is(err, nanogit.ErrObjectNotFound) {
return &provisioning.TestResults{
Code: http.StatusBadRequest,
Success: false,
Errors: []provisioning.ErrorDetails{{
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", t, "branch").String(),
Detail: detail,
}}}, nil
}
detail = fmt.Sprintf("failed to check if branch exists: %v", err)
return &provisioning.TestResults{
Code: http.StatusBadRequest,
Success: false,
Errors: []provisioning.ErrorDetails{{
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", t, "branch").String(),
Detail: detail,
}}}, nil
}
return &provisioning.TestResults{
Code: http.StatusOK,
Success: true,
}, nil
}
// Read implements provisioning.Repository.
func (r *gitRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) {
ctx, _ = r.logger(ctx, ref)
finalPath := safepath.Join(r.gitConfig.Path, filePath)
// Resolve ref to commit hash
refHash, err := r.resolveRefToHash(ctx, ref)
if err != nil {
return nil, err
}
// get root hash
// TODO: Fix GetTree in nanogit as it does not work commit hash
commit, err := r.client.GetCommit(ctx, refHash)
if err != nil {
return nil, fmt.Errorf("get commit: %w", err)
}
// Check if the path represents a directory
if safepath.IsDir(filePath) {
tree, err := r.client.GetTreeByPath(ctx, commit.Tree, finalPath)
if err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {
return nil, repository.ErrFileNotFound
}
return nil, fmt.Errorf("get tree by path: %w", err)
}
return &repository.FileInfo{
Path: filePath,
Ref: refHash.String(),
Hash: tree.Hash.String(),
}, nil
}
blob, err := r.client.GetBlobByPath(ctx, commit.Tree, finalPath)
if err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {
return nil, repository.ErrFileNotFound
}
return nil, fmt.Errorf("read blob: %w", err)
}
return &repository.FileInfo{
Path: filePath,
Ref: ref,
Data: blob.Content,
Hash: blob.Hash.String(),
}, nil
}
func (r *gitRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) {
ctx, _ = r.logger(ctx, ref)
// Resolve ref to commit hash
refHash, err := r.resolveRefToHash(ctx, ref)
if err != nil {
return nil, err
}
// Get flat tree using nanogit's GetFlatTree
tree, err := r.client.GetFlatTree(ctx, refHash)
if err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {
return nil, repository.ErrRefNotFound
}
return nil, fmt.Errorf("get flat tree: %w", err)
}
entries := make([]repository.FileTreeEntry, 0, len(tree.Entries))
for _, entry := range tree.Entries {
isBlob := entry.Type == protocol.ObjectTypeBlob
// Apply path prefix filtering
relativePath, err := safepath.RelativeTo(entry.Path, r.gitConfig.Path)
if err != nil {
// File is outside configured path, skip it
continue
}
filePath := relativePath
if !isBlob && !safepath.IsDir(filePath) {
filePath = filePath + "/"
}
converted := repository.FileTreeEntry{
Path: filePath,
// TODO: Remove size from repository.FileTreeEntry. We don't need it per se.
Size: 0, // FlatTreeEntry doesn't have size, set to 0
Hash: entry.Hash.String(),
Blob: isBlob,
}
entries = append(entries, converted)
}
return entries, nil
}
func (r *gitRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error {
if ref == "" {
ref = r.gitConfig.Branch
}
ctx, _ = r.logger(ctx, ref)
branchRef, err := r.ensureBranchExists(ctx, ref)
if err != nil {
return err
}
writer, err := r.client.NewStagedWriter(ctx, branchRef)
if err != nil {
return fmt.Errorf("create staged writer: %w", err)
}
if err := r.create(ctx, path, data, writer); err != nil {
return err
}
return r.commitAndPush(ctx, writer, comment)
}
func (r *gitRepository) create(ctx context.Context, path string, data []byte, writer nanogit.StagedWriter) error {
finalPath := safepath.Join(r.gitConfig.Path, path)
// Create .keep file if it is a directory
if safepath.IsDir(finalPath) {
if data != nil {
return apierrors.NewBadRequest("data cannot be provided for a directory")
}
finalPath = safepath.Join(finalPath, ".keep")
data = []byte{}
}
if _, err := writer.CreateBlob(ctx, finalPath, data); err != nil {
if errors.Is(err, nanogit.ErrObjectAlreadyExists) {
return repository.ErrFileAlreadyExists
}
return fmt.Errorf("create blob: %w", err)
}
return nil
}
func (r *gitRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error {
if ref == "" {
ref = r.gitConfig.Branch
}
ctx, _ = r.logger(ctx, ref)
// Check if trying to update a directory
if safepath.IsDir(path) {
return apierrors.NewBadRequest("cannot update a directory")
}
branchRef, err := r.ensureBranchExists(ctx, ref)
if err != nil {
return err
}
// Create a staged writer
writer, err := r.client.NewStagedWriter(ctx, branchRef)
if err != nil {
return fmt.Errorf("create staged writer: %w", err)
}
if err := r.update(ctx, path, data, writer); err != nil {
return err
}
return r.commitAndPush(ctx, writer, comment)
}
func (r *gitRepository) update(ctx context.Context, path string, data []byte, writer nanogit.StagedWriter) error {
// Check if trying to update a directory
if safepath.IsDir(path) {
return apierrors.NewBadRequest("cannot update a directory")
}
finalPath := safepath.Join(r.gitConfig.Path, path)
if _, err := writer.UpdateBlob(ctx, finalPath, data); err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {
return repository.ErrFileNotFound
}
return fmt.Errorf("update blob: %w", err)
}
return nil
}
func (r *gitRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error {
if ref == "" {
ref = r.gitConfig.Branch
}
ctx, _ = r.logger(ctx, ref)
_, err := r.Read(ctx, path, ref)
if err != nil && !(errors.Is(err, repository.ErrFileNotFound)) {
return fmt.Errorf("check if file exists before writing: %w", err)
}
if err == nil {
return r.Update(ctx, path, ref, data, message)
}
return r.Create(ctx, path, ref, data, message)
}
func (r *gitRepository) Delete(ctx context.Context, path, ref, comment string) error {
if ref == "" {
ref = r.gitConfig.Branch
}
ctx, _ = r.logger(ctx, ref)
branchRef, err := r.ensureBranchExists(ctx, ref)
if err != nil {
return err
}
// Create a staged writer
writer, err := r.client.NewStagedWriter(ctx, branchRef)
if err != nil {
return fmt.Errorf("create staged writer: %w", err)
}
if err := r.delete(ctx, path, writer); err != nil {
return err
}
return r.commitAndPush(ctx, writer, comment)
}
func (r *gitRepository) delete(ctx context.Context, path string, writer nanogit.StagedWriter) error {
finalPath := safepath.Join(r.gitConfig.Path, path)
// Check if it's a directory - use DeleteTree for directories, DeleteBlob for files
if safepath.IsDir(path) {
if _, err := writer.DeleteTree(ctx, finalPath); err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {
return repository.ErrFileNotFound
}
return fmt.Errorf("delete tree: %w", err)
}
} else {
if _, err := writer.DeleteBlob(ctx, finalPath); err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {
return repository.ErrFileNotFound
}
return fmt.Errorf("delete blob: %w", err)
}
}
return nil
}
func (r *gitRepository) History(_ context.Context, _ string, _ string) ([]provisioning.HistoryItem, error) {
return nil, &apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotImplemented,
Reason: metav1.StatusReasonMethodNotAllowed,
Message: "history is not supported for pure git repositories",
}}
}
func (r *gitRepository) LatestRef(ctx context.Context) (string, error) {
ctx, _ = r.logger(ctx, "")
branchRef, err := r.client.GetRef(ctx, fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch))
if err != nil {
return "", fmt.Errorf("get branch ref: %w", err)
}
return branchRef.Hash.String(), nil
}
func (r *gitRepository) CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) {
if base == "" && ref == "" {
return nil, fmt.Errorf("base and ref cannot be empty")
}
if ref == "" {
return nil, fmt.Errorf("ref cannot be empty")
}
ctx, logger := r.logger(ctx, ref)
// Resolve base ref to hash
var baseHash hash.Hash
if base != "" {
var err error
baseHash, err = r.resolveRefToHash(ctx, base)
if err != nil {
return nil, fmt.Errorf("resolve base ref: %w", err)
}
}
// Resolve ref to hash
refHash, err := r.resolveRefToHash(ctx, ref)
if err != nil {
return nil, fmt.Errorf("resolve ref: %w", err)
}
// Get commit hashes for base and ref
// Compare commits using nanogit
files, err := r.client.CompareCommits(ctx, baseHash, refHash)
if err != nil {
return nil, fmt.Errorf("compare commits: %w", err)
}
changes := make([]repository.VersionedFileChange, 0)
for _, f := range files {
switch f.Status {
case protocol.FileStatusAdded:
currentPath, err := safepath.RelativeTo(f.Path, r.gitConfig.Path)
if err != nil {
// do nothing as it's outside of configured path
continue
}
changes = append(changes, repository.VersionedFileChange{
Path: currentPath,
Ref: ref,
Action: repository.FileActionCreated,
})
case protocol.FileStatusModified:
currentPath, err := safepath.RelativeTo(f.Path, r.gitConfig.Path)
if err != nil {
// do nothing as it's outside of configured path
continue
}
changes = append(changes, repository.VersionedFileChange{
Path: currentPath,
Ref: ref,
Action: repository.FileActionUpdated,
})
case protocol.FileStatusDeleted:
currentPath, err := safepath.RelativeTo(f.Path, r.gitConfig.Path)
if err != nil {
// do nothing as it's outside of configured path
continue
}
changes = append(changes, repository.VersionedFileChange{
Ref: ref,
PreviousRef: base,
Path: currentPath,
PreviousPath: currentPath,
Action: repository.FileActionDeleted,
})
case protocol.FileStatusTypeChanged:
// Handle type changes as modifications
currentPath, err := safepath.RelativeTo(f.Path, r.gitConfig.Path)
if err != nil {
// do nothing as it's outside of configured path
continue
}
changes = append(changes, repository.VersionedFileChange{
Path: currentPath,
Ref: ref,
Action: repository.FileActionUpdated,
})
default:
logger.Error("ignore unhandled file", "file", f.Path, "status", string(f.Status))
}
}
return changes, nil
}
func (r *gitRepository) Clone(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) {
return NewStagedGitRepository(ctx, r, opts)
}
// resolveRefToHash resolves a ref (branch name or commit hash) to a commit hash
func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash.Hash, error) {
// Use default branch if ref is empty
if ref == "" {
ref = fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch)
}
// Try to parse ref as a hash first
refHash, err := hash.FromHex(ref)
if err == nil && refHash != nil {
// Valid hash, return it
return refHash, nil
}
// Not a valid hash, try to resolve as a branch reference
branchRef, err := r.client.GetRef(ctx, ref)
if err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {
return nil, fmt.Errorf("ref not found: %s: %w", ref, repository.ErrRefNotFound)
}
return nil, fmt.Errorf("get ref %s: %w", ref, err)
}
return branchRef.Hash, nil
}
// ensureBranchExists checks if a branch exists and creates it if it doesn't,
// returning the branch reference to avoid duplicate GetRef calls
func (r *gitRepository) ensureBranchExists(ctx context.Context, branchName string) (nanogit.Ref, error) {
if !repository.IsValidGitBranchName(branchName) {
return nanogit.Ref{}, &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusBadRequest,
Message: "invalid branch name",
},
}
}
// Check if branch exists by trying to get the branch reference
branchRef, err := r.client.GetRef(ctx, fmt.Sprintf("refs/heads/%s", branchName))
if err == nil {
// Branch exists, return it
logging.FromContext(ctx).Info("branch already exists", "branch", branchName)
return branchRef, nil
}
// If error is not "ref not found", return the error
if !errors.Is(err, nanogit.ErrObjectNotFound) {
return nanogit.Ref{}, fmt.Errorf("check branch exists: %w", err)
}
// Branch doesn't exist, create it based on the configured branch
srcBranch := r.gitConfig.Branch
srcRef, err := r.client.GetRef(ctx, fmt.Sprintf("refs/heads/%s", srcBranch))
if err != nil {
return nanogit.Ref{}, fmt.Errorf("get source branch ref: %w", err)
}
// Create the new branch reference
newRef := nanogit.Ref{
Name: fmt.Sprintf("refs/heads/%s", branchName),
Hash: srcRef.Hash,
}
if err := r.client.CreateRef(ctx, newRef); err != nil {
return nanogit.Ref{}, fmt.Errorf("create branch: %w", err)
}
return newRef, nil
}
// createSignature creates author and committer signatures using the context signature if available,
// falling back to default Grafana signature
func (r *gitRepository) createSignature(ctx context.Context) (nanogit.Author, nanogit.Committer) {
author := nanogit.Author{
Name: "Grafana",
Email: "noreply@grafana.com",
Time: time.Now(),
}
committer := nanogit.Committer{
Name: "Grafana",
Email: "noreply@grafana.com",
Time: time.Now(),
}
// Use signature from context if available
if sig := repository.GetAuthorSignature(ctx); sig != nil && sig.Name != "" {
author.Name = sig.Name
author.Email = sig.Email
author.Time = sig.When
committer.Name = sig.Name
committer.Email = sig.Email
committer.Time = sig.When
}
if author.Time.IsZero() {
author.Time = time.Now()
committer.Time = time.Now()
}
return author, committer
}
func (r *gitRepository) commit(ctx context.Context, writer nanogit.StagedWriter, comment string) error {
author, committer := r.createSignature(ctx)
if _, err := writer.Commit(ctx, comment, author, committer); err != nil {
return fmt.Errorf("commit changes: %w", err)
}
return nil
}
func (r *gitRepository) commitAndPush(ctx context.Context, writer nanogit.StagedWriter, comment string) error {
if err := r.commit(ctx, writer, comment); err != nil {
return err
}
if err := writer.Push(ctx); err != nil {
return fmt.Errorf("push changes: %w", err)
}
return nil
}
func (r *gitRepository) logger(ctx context.Context, ref string) (context.Context, logging.Logger) {
logger := logging.FromContext(ctx)
type containsGit int
var containsGitKey containsGit
if ctx.Value(containsGitKey) != nil {
return ctx, logging.FromContext(ctx)
}
if ref == "" {
ref = r.gitConfig.Branch
}
logger = logger.With(slog.Group("git_repository", "url", r.gitConfig.URL, "ref", ref, "nanogit", true))
ctx = logging.Context(ctx, logger)
// We want to ensure we don't add multiple git_repository keys. With doesn't deduplicate the keys...
ctx = context.WithValue(ctx, containsGitKey, true)
ctx = log.ToContext(ctx, logger)
return ctx, logger
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,125 @@
package nanogit
import (
"context"
"strings"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
"k8s.io/apimachinery/pkg/util/validation/field"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
// githubRepository is a repository implementation that integrates both a GitHub API-backed repository and a nanogit-based repository.
// It combines the features of the GitHub API with those of a standard Git repository.
// This is an interim solution to support both backends within a single repository abstraction.
// Once nanogit is fully integrated, functionality from GithubRepository should be migrated here, and this type should extend the nanogit.GitRepository interface.
type githubRepository struct {
apiRepo repository.GithubRepository
nanogitRepo repository.GitRepository
}
func NewGithubRepository(
apiRepo repository.GithubRepository,
nanogitRepo repository.GitRepository,
) repository.GithubRepository {
return &githubRepository{
apiRepo: apiRepo,
nanogitRepo: nanogitRepo,
}
}
func (r *githubRepository) Config() *provisioning.Repository {
return r.nanogitRepo.Config()
}
func (r *githubRepository) Owner() string {
return r.apiRepo.Owner()
}
func (r *githubRepository) Repo() string {
return r.apiRepo.Repo()
}
func (r *githubRepository) Client() pgh.Client {
return r.apiRepo.Client()
}
// Validate extends the nanogit repo validation with github specific validation
func (r *githubRepository) Validate() (list field.ErrorList) {
cfg := r.nanogitRepo.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 := repository.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.nanogitRepo.Validate()
}
// Test implements provisioning.Repository.
func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) {
return r.apiRepo.Test(ctx)
}
// ReadResource implements provisioning.Repository.
func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) {
return r.nanogitRepo.Read(ctx, filePath, ref)
}
func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) {
return r.nanogitRepo.ReadTree(ctx, ref)
}
func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error {
return r.nanogitRepo.Create(ctx, path, ref, data, comment)
}
func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error {
return r.nanogitRepo.Update(ctx, path, ref, data, comment)
}
func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error {
return r.nanogitRepo.Write(ctx, path, ref, data, message)
}
func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error {
return r.nanogitRepo.Delete(ctx, path, ref, comment)
}
func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) {
// Github API provides avatar URLs which nanogit does not, so we delegate to the github repo.
return r.apiRepo.History(ctx, path, ref)
}
func (r *githubRepository) LatestRef(ctx context.Context) (string, error) {
return r.nanogitRepo.LatestRef(ctx)
}
func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) {
return r.nanogitRepo.CompareFiles(ctx, base, ref)
}
// ResourceURLs implements RepositoryWithURLs.
func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*provisioning.ResourceURLs, error) {
return r.apiRepo.ResourceURLs(ctx, file)
}
func (r *githubRepository) Clone(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) {
return r.nanogitRepo.Clone(ctx, opts)
}
@@ -0,0 +1,356 @@
package nanogit
import (
"context"
"testing"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestGithubRepository(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
// Create a proper config for testing
expectedConfig := &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
GitHub: &provisioning.GitHubRepositoryConfig{
URL: "https://github.com/test/repo",
Branch: "main",
},
},
}
// Set up mock expectations for the methods that exist
gitRepo.EXPECT().Config().Return(expectedConfig)
apiRepo.EXPECT().Owner().Return("test")
apiRepo.EXPECT().Repo().Return("repo")
mockClient := pgh.NewMockClient(t)
apiRepo.EXPECT().Client().Return(mockClient)
repo := NewGithubRepository(apiRepo, gitRepo)
t.Run("delegates config to nanogit repo", func(t *testing.T) {
result := repo.Config()
require.Equal(t, expectedConfig, result)
})
t.Run("delegates owner to api repo", func(t *testing.T) {
result := repo.Owner()
require.Equal(t, "test", result)
})
t.Run("delegates repo to api repo", func(t *testing.T) {
result := repo.Repo()
require.Equal(t, "repo", result)
})
t.Run("delegates client to api repo", func(t *testing.T) {
result := repo.Client()
require.Equal(t, mockClient, result)
})
}
func TestGithubRepositoryDelegation(t *testing.T) {
ctx := context.Background()
t.Run("delegates test to api repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
expectedResult := &provisioning.TestResults{
Code: 200,
Success: true,
}
apiRepo.EXPECT().Test(ctx).Return(expectedResult, nil)
repo := NewGithubRepository(apiRepo, gitRepo)
result, err := repo.Test(ctx)
require.NoError(t, err)
require.Equal(t, expectedResult, result)
})
t.Run("delegates read to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
expectedFileInfo := &repository.FileInfo{
Path: "test.yaml",
Data: []byte("test data"),
Ref: "main",
Hash: "abc123",
}
gitRepo.EXPECT().Read(ctx, "test.yaml", "main").Return(expectedFileInfo, nil)
repo := NewGithubRepository(apiRepo, gitRepo)
result, err := repo.Read(ctx, "test.yaml", "main")
require.NoError(t, err)
require.Equal(t, expectedFileInfo, result)
})
t.Run("delegates read tree to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
expectedEntries := []repository.FileTreeEntry{
{Path: "file1.yaml", Size: 100, Hash: "hash1", Blob: true},
{Path: "dir/", Size: 0, Hash: "hash2", Blob: false},
}
gitRepo.EXPECT().ReadTree(ctx, "main").Return(expectedEntries, nil)
repo := NewGithubRepository(apiRepo, gitRepo)
result, err := repo.ReadTree(ctx, "main")
require.NoError(t, err)
require.Equal(t, expectedEntries, result)
})
t.Run("delegates create to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
data := []byte("test content")
gitRepo.EXPECT().Create(ctx, "new-file.yaml", "main", data, "Create new file").Return(nil)
repo := NewGithubRepository(apiRepo, gitRepo)
err := repo.Create(ctx, "new-file.yaml", "main", data, "Create new file")
require.NoError(t, err)
})
t.Run("delegates update to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
data := []byte("updated content")
gitRepo.EXPECT().Update(ctx, "existing-file.yaml", "main", data, "Update file").Return(nil)
repo := NewGithubRepository(apiRepo, gitRepo)
err := repo.Update(ctx, "existing-file.yaml", "main", data, "Update file")
require.NoError(t, err)
})
t.Run("delegates write to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
data := []byte("file content")
gitRepo.EXPECT().Write(ctx, "file.yaml", "main", data, "Write file").Return(nil)
repo := NewGithubRepository(apiRepo, gitRepo)
err := repo.Write(ctx, "file.yaml", "main", data, "Write file")
require.NoError(t, err)
})
t.Run("delegates delete to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
gitRepo.EXPECT().Delete(ctx, "file.yaml", "main", "Delete file").Return(nil)
repo := NewGithubRepository(apiRepo, gitRepo)
err := repo.Delete(ctx, "file.yaml", "main", "Delete file")
require.NoError(t, err)
})
t.Run("delegates history to api repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
expectedHistory := []provisioning.HistoryItem{
{
Ref: "commit1",
Message: "First commit",
Authors: []provisioning.Author{{Name: "Test User"}},
},
}
apiRepo.EXPECT().History(ctx, "file.yaml", "main").Return(expectedHistory, nil)
repo := NewGithubRepository(apiRepo, gitRepo)
result, err := repo.History(ctx, "file.yaml", "main")
require.NoError(t, err)
require.Equal(t, expectedHistory, result)
})
t.Run("delegates latest ref to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
expectedRef := "abc123def456"
gitRepo.EXPECT().LatestRef(ctx).Return(expectedRef, nil)
repo := NewGithubRepository(apiRepo, gitRepo)
result, err := repo.LatestRef(ctx)
require.NoError(t, err)
require.Equal(t, expectedRef, result)
})
t.Run("delegates compare files to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
expectedChanges := []repository.VersionedFileChange{
{
Action: repository.FileActionCreated,
Path: "new-file.yaml",
Ref: "feature-branch",
},
}
gitRepo.EXPECT().CompareFiles(ctx, "main", "feature-branch").Return(expectedChanges, nil)
repo := NewGithubRepository(apiRepo, gitRepo)
result, err := repo.CompareFiles(ctx, "main", "feature-branch")
require.NoError(t, err)
require.Equal(t, expectedChanges, result)
})
t.Run("delegates resource URLs to api repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
fileInfo := &repository.FileInfo{
Path: "dashboard.json",
Ref: "main",
Hash: "hash123",
}
expectedURLs := &provisioning.ResourceURLs{
SourceURL: "https://github.com/test/repo/blob/main/dashboard.json",
RepositoryURL: "https://github.com/test/repo",
NewPullRequestURL: "https://github.com/test/repo/compare/main...feature",
}
apiRepo.EXPECT().ResourceURLs(ctx, fileInfo).Return(expectedURLs, nil)
repo := NewGithubRepository(apiRepo, gitRepo)
result, err := repo.ResourceURLs(ctx, fileInfo)
require.NoError(t, err)
require.Equal(t, expectedURLs, result)
})
t.Run("delegates clone to nanogit repo", func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
mockClonedRepo := repository.NewMockClonedRepository(t)
opts := repository.CloneOptions{
CreateIfNotExists: true,
PushOnWrites: true,
}
gitRepo.EXPECT().Clone(ctx, opts).Return(mockClonedRepo, nil)
repo := NewGithubRepository(apiRepo, gitRepo)
result, err := repo.Clone(ctx, opts)
require.NoError(t, err)
require.Equal(t, mockClonedRepo, result)
})
}
func TestGithubRepositoryValidation(t *testing.T) {
tests := []struct {
name string
config *provisioning.Repository
expectedErrors int
}{
{
name: "missing github config",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
},
},
expectedErrors: 1,
},
{
name: "missing github url",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
},
expectedErrors: 1,
},
{
name: "invalid github url",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
GitHub: &provisioning.GitHubRepositoryConfig{
URL: "invalid-url",
Branch: "main",
},
},
},
expectedErrors: 1,
},
{
name: "non-github url",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
GitHub: &provisioning.GitHubRepositoryConfig{
URL: "https://gitlab.com/test/repo",
Branch: "main",
},
},
},
expectedErrors: 1,
},
{
name: "valid github config",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
GitHub: &provisioning.GitHubRepositoryConfig{
URL: "https://github.com/test/repo",
Branch: "main",
},
},
},
expectedErrors: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
apiRepo := repository.NewMockGithubRepository(t)
gitRepo := repository.NewMockGitRepository(t)
// Set up mock expectations
gitRepo.EXPECT().Config().Return(tt.config)
if tt.expectedErrors == 0 {
// If no validation errors expected, nanogit validation should be called
gitRepo.EXPECT().Validate().Return(field.ErrorList{})
}
repo := NewGithubRepository(apiRepo, gitRepo)
result := repo.Validate()
require.Len(t, result, tt.expectedErrors)
})
}
}
@@ -0,0 +1,193 @@
package nanogit
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
"github.com/grafana/nanogit"
)
// stagedGitRepository implements repository.ClonedRepository by wrapping a gitRepository
// FIXME: this is a hack until we can delete the go-git cloned implementation
// once that happens we could do more magic here.
type stagedGitRepository struct {
*gitRepository
opts repository.CloneOptions
writer nanogit.StagedWriter
}
func NewStagedGitRepository(ctx context.Context, repo *gitRepository, opts repository.CloneOptions) (repository.ClonedRepository, error) {
if opts.BeforeFn != nil {
if err := opts.BeforeFn(); err != nil {
return nil, err
}
}
if opts.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
defer cancel()
}
ref, err := repo.client.GetRef(ctx, "refs/heads/"+repo.gitConfig.Branch)
if err != nil {
// TODO: opts.CreateIfNotExists doesn't make sense in the context of the staged repository
// because we only support the branch that is passed in.
// we should probably add branch to the repository.CloneOptions which should be repurposed
// as some kind of branch creation options.
return nil, err
}
writer, err := repo.client.NewStagedWriter(ctx, ref)
if err != nil {
return nil, fmt.Errorf("build staged writer: %w", err)
}
return &stagedGitRepository{
gitRepository: repo,
opts: opts,
writer: writer,
}, nil
}
func (r *stagedGitRepository) Read(ctx context.Context, path, ref string) (*repository.FileInfo, error) {
if ref != "" && ref != r.gitConfig.Branch {
return nil, errors.New("ref is not supported for staged repository")
}
// TODO: the read in the cloned is simplied used to check if a folder exists,
// We should fix the usage and the interface so that it's not needed to load the entire blob
return r.gitRepository.Read(ctx, path, ref)
}
func (r *stagedGitRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) {
if ref != "" && ref != r.gitConfig.Branch {
return nil, errors.New("ref is not supported for staged repository")
}
ref = ""
// TODO: I think we don't need this for cloned repository currently.
// we should probably remove it from the interface or construct this tree from the writer itself
return r.gitRepository.ReadTree(ctx, ref)
}
func (r *stagedGitRepository) Create(ctx context.Context, path, ref string, data []byte, message string) error {
if ref != "" && ref != r.gitConfig.Branch {
return errors.New("ref is not supported for staged repository")
}
if err := r.create(ctx, path, data, r.writer); err != nil {
return err
}
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
if r.opts.PushOnWrites {
return r.Push(ctx, repository.PushOptions{})
}
return nil
}
func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data []byte, message string) error {
if ref != "" && ref != r.gitConfig.Branch {
return errors.New("ref is not supported for staged repository")
}
ok, err := r.writer.BlobExists(ctx, path)
if err != nil {
return fmt.Errorf("check if file exists: %w", err)
}
if !ok {
if err := r.create(ctx, path, data, r.writer); err != nil {
return err
}
} else {
if err := r.update(ctx, path, data, r.writer); err != nil {
return err
}
}
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
if r.opts.PushOnWrites {
return r.Push(ctx, repository.PushOptions{})
}
return nil
}
func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data []byte, message string) error {
if ref != "" && ref != r.gitConfig.Branch {
return errors.New("ref is not supported for staged repository")
}
if safepath.IsDir(path) {
return errors.New("cannot update a directory in a staged repository")
}
if err := r.update(ctx, path, data, r.writer); err != nil {
return err
}
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
if r.opts.PushOnWrites {
return r.Push(ctx, repository.PushOptions{})
}
return nil
}
func (r *stagedGitRepository) Delete(ctx context.Context, path, ref, message string) error {
if ref != "" && ref != r.gitConfig.Branch {
return errors.New("ref is not supported for staged repository")
}
if err := r.delete(ctx, path, r.writer); err != nil {
return err
}
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
if r.opts.PushOnWrites {
return r.Push(ctx, repository.PushOptions{})
}
return nil
}
func (r *stagedGitRepository) Push(ctx context.Context, opts repository.PushOptions) error {
if opts.BeforeFn != nil {
if err := opts.BeforeFn(); err != nil {
return err
}
}
if opts.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
defer cancel()
}
return r.writer.Push(ctx)
}
func (r *stagedGitRepository) Remove(_ context.Context) error {
// Since we're using nanogit which doesn't actually clone the repository,
// we don't need to do anything here as there's nothing to clean up
return nil
}
@@ -0,0 +1,941 @@
package nanogit
import (
"context"
"errors"
"testing"
"time"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/nanogit"
"github.com/grafana/nanogit/mocks"
"github.com/grafana/nanogit/protocol/hash"
"github.com/stretchr/testify/require"
)
func TestNewStagedGitRepository(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeClient)
opts repository.CloneOptions
wantError error
}{
{
name: "succeeds with default options",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{1, 2, 3},
}, nil)
mockWriter := &mocks.FakeStagedWriter{}
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
},
wantError: nil,
},
{
name: "succeeds with BeforeFn",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{1, 2, 3},
}, nil)
mockWriter := &mocks.FakeStagedWriter{}
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
BeforeFn: func() error {
return nil
},
},
wantError: nil,
},
{
name: "succeeds with timeout",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{1, 2, 3},
}, nil)
mockWriter := &mocks.FakeStagedWriter{}
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
Timeout: time.Second * 5,
},
wantError: nil,
},
{
name: "fails with BeforeFn error",
setupMock: func(mockClient *mocks.FakeClient) {
// No setup needed as BeforeFn fails first
},
opts: repository.CloneOptions{
BeforeFn: func() error {
return errors.New("before function failed")
},
},
wantError: errors.New("before function failed"),
},
{
name: "fails with GetRef error",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("ref not found"))
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
},
wantError: errors.New("ref not found"),
},
{
name: "fails with NewStagedWriter error",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{1, 2, 3},
}, nil)
mockClient.NewStagedWriterReturns(nil, errors.New("failed to create writer"))
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
},
wantError: errors.New("build staged writer: failed to create writer"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := &mocks.FakeClient{}
tt.setupMock(mockClient)
gitRepo := &gitRepository{
client: mockClient,
gitConfig: RepositoryConfig{
Branch: "main",
URL: "https://git.example.com/repo.git",
Token: "token123",
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: "test_type",
},
},
}
stagedRepo, err := NewStagedGitRepository(context.Background(), gitRepo, tt.opts)
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
} else {
require.NoError(t, err)
require.NotNil(t, stagedRepo)
// Compare opts fields individually since function pointers can't be compared directly
actualOpts := stagedRepo.(*stagedGitRepository).opts
require.Equal(t, tt.opts.CreateIfNotExists, actualOpts.CreateIfNotExists)
require.Equal(t, tt.opts.PushOnWrites, actualOpts.PushOnWrites)
require.Equal(t, tt.opts.MaxSize, actualOpts.MaxSize)
require.Equal(t, tt.opts.Timeout, actualOpts.Timeout)
require.Equal(t, tt.opts.Progress, actualOpts.Progress)
// BeforeFn is a function pointer, so we just check if both are nil or both are not nil
require.Equal(t, tt.opts.BeforeFn == nil, actualOpts.BeforeFn == nil)
}
})
}
}
func TestStagedGitRepository_Read(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeClient)
path string
ref string
wantError error
}{
{
name: "succeeds with empty ref",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{1, 2, 3},
}, nil)
mockClient.GetCommitReturns(&nanogit.Commit{
Tree: hash.Hash{4, 5, 6},
}, nil)
mockClient.GetBlobByPathReturns(&nanogit.Blob{
Content: []byte("file content"),
Hash: hash.Hash{7, 8, 9},
}, nil)
},
path: "test.yaml",
ref: "",
wantError: nil,
},
{
name: "succeeds with matching ref",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{1, 2, 3},
}, nil)
mockClient.GetCommitReturns(&nanogit.Commit{
Tree: hash.Hash{4, 5, 6},
}, nil)
mockClient.GetBlobByPathReturns(&nanogit.Blob{
Content: []byte("file content"),
Hash: hash.Hash{7, 8, 9},
}, nil)
},
path: "test.yaml",
ref: "main",
wantError: nil,
},
{
name: "fails with unsupported ref",
setupMock: func(_ *mocks.FakeClient) {
// No setup needed as error occurs before client calls
},
path: "test.yaml",
ref: "feature-branch",
wantError: errors.New("ref is not supported for staged repository"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := &mocks.FakeClient{}
tt.setupMock(mockClient)
stagedRepo := createTestStagedRepository(mockClient)
fileInfo, err := stagedRepo.Read(context.Background(), tt.path, tt.ref)
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
} else {
require.NoError(t, err)
require.NotNil(t, fileInfo)
require.Equal(t, tt.path, fileInfo.Path)
}
})
}
}
func TestStagedGitRepository_ReadTree(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeClient)
ref string
wantError error
}{
{
name: "succeeds with empty ref",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{1, 2, 3},
}, nil)
mockClient.GetFlatTreeReturns(&nanogit.FlatTree{
Entries: []nanogit.FlatTreeEntry{
{
Path: "configs/test.yaml",
Hash: hash.Hash{4, 5, 6},
},
},
}, nil)
},
ref: "",
wantError: nil,
},
{
name: "fails with unsupported ref",
setupMock: func(mockClient *mocks.FakeClient) {
// No setup needed as error occurs before client calls
},
ref: "feature-branch",
wantError: errors.New("ref is not supported for staged repository"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := &mocks.FakeClient{}
tt.setupMock(mockClient)
stagedRepo := createTestStagedRepository(mockClient)
entries, err := stagedRepo.ReadTree(context.Background(), tt.ref)
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
} else {
require.NoError(t, err)
require.NotNil(t, entries)
}
})
}
}
func TestStagedGitRepository_Create(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.CloneOptions
path string
ref string
data []byte
message string
wantError error
expectPush bool
}{
{
name: "succeeds with empty ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Create test file",
wantError: nil,
expectPush: false,
},
{
name: "succeeds with matching ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
opts: repository.CloneOptions{
PushOnWrites: true,
},
path: "test.yaml",
ref: "main",
data: []byte("content"),
message: "Create test file",
wantError: nil,
expectPush: true,
},
{
name: "fails with unsupported ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "feature-branch",
data: []byte("content"),
message: "Create test file",
wantError: errors.New("ref is not supported for staged repository"),
},
{
name: "fails with create blob error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create blob failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Create test file",
wantError: errors.New("create blob: create blob failed"),
},
{
name: "fails with commit error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Create test file",
wantError: errors.New("commit changes: commit failed"),
},
{
name: "fails with push error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(errors.New("push failed"))
},
opts: repository.CloneOptions{
PushOnWrites: true,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Create test file",
wantError: errors.New("push failed"),
expectPush: true, // Push is still called even though it fails
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
tt.setupMock(mockWriter)
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, tt.opts)
err := stagedRepo.Create(context.Background(), tt.path, tt.ref, tt.data, tt.message)
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
} else {
require.NoError(t, err)
}
// Verify push behavior
if tt.expectPush {
require.Equal(t, 1, mockWriter.PushCallCount())
} else if tt.wantError == nil {
require.Equal(t, 0, mockWriter.PushCallCount())
}
})
}
}
func TestStagedGitRepository_Write(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.CloneOptions
path string
ref string
data []byte
message string
fileExists bool
wantError error
expectPush bool
}{
{
name: "succeeds with empty ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.BlobExistsReturns(false, nil)
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Write test file",
fileExists: false,
wantError: nil,
expectPush: false,
},
{
name: "succeeds with matching ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.BlobExistsReturns(true, nil)
mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
opts: repository.CloneOptions{
PushOnWrites: true,
},
path: "test.yaml",
ref: "main",
data: []byte("updated content"),
message: "Update test file",
fileExists: true,
wantError: nil,
expectPush: true,
},
{
name: "fails with unsupported ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "feature-branch",
data: []byte("content"),
message: "Write test file",
wantError: errors.New("ref is not supported for staged repository"),
},
{
name: "fails with blob exists check error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.BlobExistsReturns(false, errors.New("blob exists check failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Write test file",
wantError: errors.New("check if file exists: blob exists check failed"),
},
{
name: "fails with create error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.BlobExistsReturns(false, nil)
mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Write test file",
wantError: errors.New("create blob: create failed"),
},
{
name: "fails with update error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.BlobExistsReturns(true, nil)
mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Write test file",
wantError: errors.New("update blob: update failed"),
},
{
name: "fails with commit error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.BlobExistsReturns(false, nil)
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Write test file",
wantError: errors.New("commit changes: commit failed"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
tt.setupMock(mockWriter)
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, tt.opts)
err := stagedRepo.Write(context.Background(), tt.path, tt.ref, tt.data, tt.message)
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
} else {
require.NoError(t, err)
}
// Verify push behavior
if tt.expectPush {
require.Equal(t, 1, mockWriter.PushCallCount())
} else if tt.wantError == nil {
require.Equal(t, 0, mockWriter.PushCallCount())
}
})
}
}
func TestStagedGitRepository_Update(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.CloneOptions
path string
ref string
data []byte
message string
wantError error
expectPush bool
}{
{
name: "succeeds with empty ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("updated content"),
message: "Update test file",
wantError: nil,
expectPush: false,
},
{
name: "succeeds with matching ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
opts: repository.CloneOptions{
PushOnWrites: true,
},
path: "test.yaml",
ref: "main",
data: []byte("updated content"),
message: "Update test file",
wantError: nil,
expectPush: true,
},
{
name: "fails with unsupported ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "feature-branch",
data: []byte("content"),
message: "Update test file",
wantError: errors.New("ref is not supported for staged repository"),
},
{
name: "fails with directory update",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "directory/",
ref: "",
data: []byte("content"),
message: "Update directory",
wantError: errors.New("cannot update a directory in a staged repository"),
},
{
name: "fails with update blob error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update blob failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Update test file",
wantError: errors.New("update blob: update blob failed"),
},
{
name: "fails with commit error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Update test file",
wantError: errors.New("commit changes: commit failed"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
tt.setupMock(mockWriter)
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, tt.opts)
err := stagedRepo.Update(context.Background(), tt.path, tt.ref, tt.data, tt.message)
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
} else {
require.NoError(t, err)
}
// Verify push behavior
if tt.expectPush {
require.Equal(t, 1, mockWriter.PushCallCount())
} else if tt.wantError == nil {
require.Equal(t, 0, mockWriter.PushCallCount())
}
})
}
}
func TestStagedGitRepository_Delete(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.CloneOptions
path string
ref string
message string
wantError error
expectPush bool
}{
{
name: "succeeds with empty ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.DeleteBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
message: "Delete test file",
wantError: nil,
expectPush: false,
},
{
name: "succeeds with matching ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.DeleteTreeReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
opts: repository.CloneOptions{
PushOnWrites: true,
},
path: "testdir/",
ref: "main",
message: "Delete test directory",
wantError: nil,
expectPush: true,
},
{
name: "fails with unsupported ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "feature-branch",
message: "Delete test file",
wantError: errors.New("ref is not supported for staged repository"),
},
{
name: "fails with delete blob error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.DeleteBlobReturns(hash.Hash{}, errors.New("delete blob failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
message: "Delete test file",
wantError: errors.New("delete blob: delete blob failed"),
},
{
name: "fails with commit error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.DeleteBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
opts: repository.CloneOptions{
PushOnWrites: false,
},
path: "test.yaml",
ref: "",
message: "Delete test file",
wantError: errors.New("commit changes: commit failed"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
tt.setupMock(mockWriter)
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, tt.opts)
err := stagedRepo.Delete(context.Background(), tt.path, tt.ref, tt.message)
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
} else {
require.NoError(t, err)
}
// Verify push behavior
if tt.expectPush {
require.Equal(t, 1, mockWriter.PushCallCount())
} else if tt.wantError == nil {
require.Equal(t, 0, mockWriter.PushCallCount())
}
})
}
}
func TestStagedGitRepository_Push(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.PushOptions
wantError error
expectCalls int
}{
{
name: "succeeds with empty ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(nil)
},
opts: repository.PushOptions{},
wantError: nil,
expectCalls: 1,
},
{
name: "succeeds with matching ref",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(nil)
},
opts: repository.PushOptions{
BeforeFn: func() error {
return nil
},
},
wantError: nil,
expectCalls: 1,
},
{
name: "succeeds with timeout",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(nil)
},
opts: repository.PushOptions{
Timeout: time.Second * 5,
},
wantError: nil,
expectCalls: 1,
},
{
name: "fails with before fn error",
setupMock: func(_ *mocks.FakeStagedWriter) {
// No setup needed as BeforeFn fails first
},
opts: repository.PushOptions{
BeforeFn: func() error {
return errors.New("before function failed")
},
},
wantError: errors.New("before function failed"),
expectCalls: 0,
},
{
name: "fails with push error",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(errors.New("push failed"))
},
opts: repository.PushOptions{},
wantError: errors.New("push failed"),
expectCalls: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
tt.setupMock(mockWriter)
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{})
err := stagedRepo.Push(context.Background(), tt.opts)
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
} else {
require.NoError(t, err)
}
require.Equal(t, tt.expectCalls, mockWriter.PushCallCount())
})
}
}
func TestStagedGitRepository_Remove(t *testing.T) {
t.Run("succeeds with remove", func(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{})
err := stagedRepo.Remove(context.Background())
require.NoError(t, err)
// No mock calls expected since Remove is a no-op
})
}
// Helper functions for creating test instances
func createTestStagedRepository(mockClient *mocks.FakeClient) *stagedGitRepository {
mockWriter := &mocks.FakeStagedWriter{}
return createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{}, mockClient)
}
func createTestStagedRepositoryWithWriter(mockWriter *mocks.FakeStagedWriter, opts repository.CloneOptions, mockClient ...*mocks.FakeClient) *stagedGitRepository {
var client nanogit.Client
if len(mockClient) > 0 {
client = mockClient[0]
} else {
// Create a default mock client for tests that don't need specific behavior
defaultClient := &mocks.FakeClient{}
client = defaultClient
}
gitRepo := &gitRepository{
client: client,
gitConfig: RepositoryConfig{
Branch: "main",
URL: "https://git.example.com/repo.git",
Token: "token123",
Path: "configs",
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: "test_type",
},
},
}
return &stagedGitRepository{
gitRepository: gitRepo,
opts: opts,
writer: mockWriter,
}
}
@@ -38,6 +38,20 @@ var ErrFileNotFound error = &apierrors.StatusError{ErrStatus: metav1.Status{
Message: "file not found",
}}
var ErrRefNotFound error = &apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotFound,
Reason: metav1.StatusReasonNotFound,
Message: "ref not found",
}}
var ErrFileAlreadyExists error = &apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusConflict,
Reason: metav1.StatusReasonAlreadyExists,
Message: "file already exists",
}}
type FileInfo struct {
// Path to the file on disk.
// No leading or trailing slashes will be contained within.
@@ -6,16 +6,19 @@ import (
"path/filepath"
"strings"
"github.com/grafana/grafana-app-sdk/logging"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
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/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"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/rendering"
grafanasecrets "github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
@@ -34,6 +37,7 @@ type WebhookExtraBuilder struct {
func ProvideWebhooks(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
// FIXME: use multi-tenant service when one exists. In this state, we can't make this a multi-tenant service!
secretsSvc grafanasecrets.Service,
ghFactory *github.Factory,
@@ -64,6 +68,7 @@ func ProvideWebhooks(
pullRequestWorker := pullrequest.NewPullRequestWorker(evaluator, commenter)
return NewWebhookExtra(
features,
render,
webhook,
urlProvider,
@@ -80,6 +85,7 @@ func ProvideWebhooks(
// WebhookExtra implements the Extra interface for webhooks
// to wrap around
type WebhookExtra struct {
features featuremgmt.FeatureToggles
render *renderConnector
webhook *webhookConnector
urlProvider func(namespace string) string
@@ -91,6 +97,7 @@ type WebhookExtra struct {
}
func NewWebhookExtra(
features featuremgmt.FeatureToggles,
render *renderConnector,
webhook *webhookConnector,
urlProvider func(namespace string) string,
@@ -101,6 +108,7 @@ func NewWebhookExtra(
workers []jobs.Worker,
) *WebhookExtra {
return &WebhookExtra{
features: features,
render: render,
webhook: webhook,
urlProvider: urlProvider,
@@ -176,11 +184,39 @@ func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Reposit
return gogit.Clone(ctx, e.clonedir, r, opts, e.secrets)
}
basicRepo, err := repository.NewGitHub(ctx, r, e.ghFactory, e.secrets, cloneFn)
apiRepo, err := repository.NewGitHub(ctx, r, e.ghFactory, e.secrets, cloneFn)
if err != nil {
return nil, err
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")
ghCfg := r.Spec.GitHub
if ghCfg == nil {
return nil, fmt.Errorf("github configuration is required for nano git")
}
gitCfg := nanogit.RepositoryConfig{
URL: ghCfg.URL,
Branch: ghCfg.Branch,
Path: ghCfg.Path,
Token: ghCfg.Token,
EncryptedToken: ghCfg.EncryptedToken,
}
nanogitRepo, err := nanogit.NewGitRepository(ctx, e.secrets, r, gitCfg)
if err != nil {
return nil, fmt.Errorf("error creating nanogit repository: %w", err)
}
basicRepo := nanogit.NewGithubRepository(apiRepo, nanogitRepo)
return NewGithubWebhookRepository(basicRepo, webhookURL, e.secrets), nil
}
+7
View File
@@ -351,6 +351,13 @@ var (
RequiresRestart: true,
Owner: grafanaAppPlatformSquad,
},
{
Name: "nanoGit",
Description: "Use experimental git library for provisioning",
Stage: FeatureStageExperimental,
RequiresRestart: true,
Owner: grafanaAppPlatformSquad,
},
{
Name: "grafanaAPIServerEnsureKubectlAccess",
Description: "Start an additional https handler and write kubectl options",
+1
View File
@@ -45,6 +45,7 @@ mlExpressions,experimental,@grafana/alerting-squad,false,false,false
datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false
grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false
provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false
nanoGit,experimental,@grafana/grafana-app-platform-squad,false,true,false
grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false
featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false
awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
45 datasourceAPIServers experimental @grafana/grafana-app-platform-squad false true false
46 grafanaAPIServerWithExperimentalAPIs experimental @grafana/grafana-app-platform-squad true true false
47 provisioning experimental @grafana/grafana-app-platform-squad false true false
48 nanoGit experimental @grafana/grafana-app-platform-squad false true false
49 grafanaAPIServerEnsureKubectlAccess experimental @grafana/grafana-app-platform-squad true true false
50 featureToggleAdminPage experimental @grafana/grafana-operator-experience-squad false true false
51 awsAsyncQueryCaching GA @grafana/aws-datasources false false false
+4
View File
@@ -191,6 +191,10 @@ const (
// Next generation provisioning... and git
FlagProvisioning = "provisioning"
// FlagNanoGit
// Use experimental git library for provisioning
FlagNanoGit = "nanoGit"
// FlagGrafanaAPIServerEnsureKubectlAccess
// Start an additional https handler and write kubectl options
FlagGrafanaAPIServerEnsureKubectlAccess = "grafanaAPIServerEnsureKubectlAccess"
+13
View File
@@ -2236,6 +2236,19 @@
"codeowner": "@grafana/search-and-storage"
}
},
{
"metadata": {
"name": "nanoGit",
"resourceVersion": "1750087021356",
"creationTimestamp": "2025-06-16T15:17:01Z"
},
"spec": {
"description": "Use experimental git library for provisioning",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad",
"requiresRestart": true
}
},
{
"metadata": {
"name": "nestedFolders",