Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs

This commit is contained in:
Ryan McKinley
2025-07-10 09:51:51 -07:00
196 changed files with 5867 additions and 14047 deletions

View File

@@ -3462,8 +3462,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "0"]
],
"public/app/plugins/datasource/jaeger/datasource.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/loki/LanguageProvider.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]

View File

@@ -16,6 +16,7 @@ jobs:
contents: read
outputs:
changed: ${{ steps.detect-changes.outputs.frontend }}
prettier: ${{ steps.detect-changes.outputs.frontend == 'true' || steps.detect-changes.outputs.docs == 'true' }}
steps:
- uses: actions/checkout@v4
with:
@@ -34,7 +35,7 @@ jobs:
id-token: write
# Run this workflow only for PRs from forks; if it gets merged into `main` or `release-*`,
# the `lint-frontend-prettier-enterprise` workflow will run instead
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.changed == 'true'
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.prettier == 'true'
name: Lint
runs-on: ubuntu-latest
steps:
@@ -55,7 +56,7 @@ jobs:
contents: read
id-token: write
# Run this workflow for non-PR events (like pushes to `main` or `release-*`) OR for internal PRs (PRs not from forks)
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.changed == 'true'
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.prettier == 'true'
name: Lint
runs-on: ubuntu-latest
steps:

View File

@@ -1,3 +1,22 @@
APP_SDK_VERSION := v0.39.2
APP_SDK_DIR := $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION)
APP_SDK_BIN := $(APP_SDK_DIR)/grafana-app-sdk
.PHONY: install-app-sdk
install-app-sdk: $(APP_SDK_BIN) ## Install the Grafana App SDK
$(APP_SDK_BIN):
@echo "Installing Grafana App SDK version $(APP_SDK_VERSION)"
@mkdir -p $(APP_SDK_DIR)
# The only way to install specific versions of binaries using `go install`
# is by setting GOBIN to the directory you want to install the binary to.
GOBIN=$(APP_SDK_DIR) go install github.com/grafana/grafana-app-sdk/cmd/grafana-app-sdk@$(APP_SDK_VERSION)
@touch $@
.PHONY: update-app-sdk
update-app-sdk: ## Update the Grafana App SDK dependency in go.mod
go get github.com/grafana/grafana-app-sdk@$(APP_SDK_VERSION)
.PHONY: generate
generate:
@grafana-app-sdk generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none
generate: ## Run Grafana App SDK code generation
@$(APP_SDK_BIN) generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none

View File

@@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/advisor
go 1.24.4
require (
github.com/grafana/grafana-app-sdk v0.39.0
k8s.io/apimachinery v0.33.1
github.com/grafana/grafana-app-sdk v0.39.2
k8s.io/apimachinery v0.33.2
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
)
@@ -33,7 +33,7 @@ require (
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb // indirect
github.com/grafana/grafana-app-sdk/logging v0.38.2 // indirect
github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -79,9 +79,9 @@ require (
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.1 // indirect
k8s.io/apiextensions-apiserver v0.33.1 // indirect
k8s.io/client-go v0.33.1 // indirect
k8s.io/api v0.33.2 // indirect
k8s.io/apiextensions-apiserver v0.33.2 // indirect
k8s.io/client-go v0.33.2 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect

View File

@@ -63,11 +63,14 @@ github.com/grafana/grafana-app-sdk v0.30.0/go.mod h1:jhfqNIovb+Mes2vdMf9iMCWQkp1
github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs=
github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI=
github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY=
github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8=
github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY=
github.com/grafana/grafana-app-sdk/logging v0.29.0 h1:mgbXaAf33aFwqwGVeaX30l8rkeAJH0iACgX5Rn6YkN4=
github.com/grafana/grafana-app-sdk/logging v0.29.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM=
github.com/grafana/grafana-app-sdk/logging v0.30.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM=
github.com/grafana/grafana-app-sdk/logging v0.35.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
@@ -302,21 +305,25 @@ k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0=
k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0=
k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw=
k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto=
k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss=
k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA=
k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8=
k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg=
k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=

View File

@@ -24,5 +24,8 @@ type CheckMetadata struct {
// NewCheckMetadata creates a new CheckMetadata object.
func NewCheckMetadata() *CheckMetadata {
return &CheckMetadata{}
return &CheckMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}
}

View File

@@ -18,8 +18,11 @@ import (
type Check struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
Spec CheckSpec `json:"spec" yaml:"spec"`
CheckStatus CheckStatus `json:"status" yaml:"status"`
// Spec is the spec of the Check
Spec CheckSpec `json:"spec" yaml:"spec"`
Status CheckStatus `json:"status" yaml:"status"`
}
func (o *Check) GetSpec() any {
@@ -37,14 +40,14 @@ func (o *Check) SetSpec(spec any) error {
func (o *Check) GetSubresources() map[string]any {
return map[string]any{
"status": o.CheckStatus,
"status": o.Status,
}
}
func (o *Check) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.CheckStatus, true
return o.Status, true
default:
return nil, false
}
@@ -57,7 +60,7 @@ func (o *Check) SetSubresource(name string, value any) error {
if !ok {
return fmt.Errorf("cannot set status type %#v, not of type CheckStatus", value)
}
o.CheckStatus = cast
o.Status = cast
return nil
default:
return fmt.Errorf("subresource '%s' does not exist", name)
@@ -219,6 +222,20 @@ func (o *Check) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *Check) DeepCopy() *Check {
cpy := &Check{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *Check) DeepCopyInto(dst *Check) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
o.Status.DeepCopyInto(&dst.Status)
}
// Interface compliance compile-time check
var _ resource.Object = &Check{}
@@ -262,5 +279,41 @@ func (o *CheckList) SetItems(items []resource.Object) {
}
}
func (o *CheckList) DeepCopy() *CheckList {
cpy := &CheckList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *CheckList) DeepCopyInto(dst *CheckList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &CheckList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *CheckSpec) DeepCopy() *CheckSpec {
cpy := &CheckSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *CheckSpec) DeepCopyInto(dst *CheckSpec) {
resource.CopyObjectInto(dst, s)
}
// DeepCopy creates a full deep copy of CheckStatus
func (s *CheckStatus) DeepCopy() *CheckStatus {
cpy := &CheckStatus{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies CheckStatus into another CheckStatus object
func (s *CheckStatus) DeepCopyInto(dst *CheckStatus) {
resource.CopyObjectInto(dst, s)
}

View File

@@ -3,16 +3,18 @@
package v0alpha1
// +k8s:openapi-gen=true
type CheckErrorLink struct {
// URL to a page with more information about the error
Url string `json:"url"`
// Human readable error message
Message string `json:"message"`
type CheckReport struct {
// Number of elements analyzed
Count int64 `json:"count"`
// List of failures
Failures []CheckReportFailure `json:"failures"`
}
// NewCheckErrorLink creates a new CheckErrorLink object.
func NewCheckErrorLink() *CheckErrorLink {
return &CheckErrorLink{}
// NewCheckReport creates a new CheckReport object.
func NewCheckReport() *CheckReport {
return &CheckReport{
Failures: []CheckReportFailure{},
}
}
// +k8s:openapi-gen=true
@@ -33,7 +35,22 @@ type CheckReportFailure struct {
// NewCheckReportFailure creates a new CheckReportFailure object.
func NewCheckReportFailure() *CheckReportFailure {
return &CheckReportFailure{}
return &CheckReportFailure{
Links: []CheckErrorLink{},
}
}
// +k8s:openapi-gen=true
type CheckErrorLink struct {
// URL to a page with more information about the error
Url string `json:"url"`
// Human readable error message
Message string `json:"message"`
}
// NewCheckErrorLink creates a new CheckErrorLink object.
func NewCheckErrorLink() *CheckErrorLink {
return &CheckErrorLink{}
}
// +k8s:openapi-gen=true
@@ -56,7 +73,7 @@ func NewCheckstatusOperatorState() *CheckstatusOperatorState {
// +k8s:openapi-gen=true
type CheckStatus struct {
Report CheckV0alpha1StatusReport `json:"report"`
Report CheckReport `json:"report"`
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]CheckstatusOperatorState `json:"operatorStates,omitempty"`
@@ -67,7 +84,7 @@ type CheckStatus struct {
// NewCheckStatus creates a new CheckStatus object.
func NewCheckStatus() *CheckStatus {
return &CheckStatus{
Report: *NewCheckV0alpha1StatusReport(),
Report: *NewCheckReport(),
}
}
@@ -87,16 +104,3 @@ const (
CheckStatusOperatorStateStateInProgress CheckStatusOperatorStateState = "in_progress"
CheckStatusOperatorStateStateFailed CheckStatusOperatorStateState = "failed"
)
// +k8s:openapi-gen=true
type CheckV0alpha1StatusReport struct {
// Number of elements analyzed
Count int64 `json:"count"`
// List of failures
Failures []CheckReportFailure `json:"failures"`
}
// NewCheckV0alpha1StatusReport creates a new CheckV0alpha1StatusReport object.
func NewCheckV0alpha1StatusReport() *CheckV0alpha1StatusReport {
return &CheckV0alpha1StatusReport{}
}

View File

@@ -24,5 +24,8 @@ type CheckTypeMetadata struct {
// NewCheckTypeMetadata creates a new CheckTypeMetadata object.
func NewCheckTypeMetadata() *CheckTypeMetadata {
return &CheckTypeMetadata{}
return &CheckTypeMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}
}

View File

@@ -18,8 +18,11 @@ import (
type CheckType struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
Spec CheckTypeSpec `json:"spec" yaml:"spec"`
CheckTypeStatus CheckTypeStatus `json:"status" yaml:"status"`
// Spec is the spec of the CheckType
Spec CheckTypeSpec `json:"spec" yaml:"spec"`
Status CheckTypeStatus `json:"status" yaml:"status"`
}
func (o *CheckType) GetSpec() any {
@@ -37,14 +40,14 @@ func (o *CheckType) SetSpec(spec any) error {
func (o *CheckType) GetSubresources() map[string]any {
return map[string]any{
"status": o.CheckTypeStatus,
"status": o.Status,
}
}
func (o *CheckType) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.CheckTypeStatus, true
return o.Status, true
default:
return nil, false
}
@@ -57,7 +60,7 @@ func (o *CheckType) SetSubresource(name string, value any) error {
if !ok {
return fmt.Errorf("cannot set status type %#v, not of type CheckTypeStatus", value)
}
o.CheckTypeStatus = cast
o.Status = cast
return nil
default:
return fmt.Errorf("subresource '%s' does not exist", name)
@@ -219,6 +222,20 @@ func (o *CheckType) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *CheckType) DeepCopy() *CheckType {
cpy := &CheckType{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *CheckType) DeepCopyInto(dst *CheckType) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
o.Status.DeepCopyInto(&dst.Status)
}
// Interface compliance compile-time check
var _ resource.Object = &CheckType{}
@@ -262,5 +279,41 @@ func (o *CheckTypeList) SetItems(items []resource.Object) {
}
}
func (o *CheckTypeList) DeepCopy() *CheckTypeList {
cpy := &CheckTypeList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *CheckTypeList) DeepCopyInto(dst *CheckTypeList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &CheckTypeList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *CheckTypeSpec) DeepCopy() *CheckTypeSpec {
cpy := &CheckTypeSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *CheckTypeSpec) DeepCopyInto(dst *CheckTypeSpec) {
resource.CopyObjectInto(dst, s)
}
// DeepCopy creates a full deep copy of CheckTypeStatus
func (s *CheckTypeStatus) DeepCopy() *CheckTypeStatus {
cpy := &CheckTypeStatus{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies CheckTypeStatus into another CheckTypeStatus object
func (s *CheckTypeStatus) DeepCopyInto(dst *CheckTypeStatus) {
resource.CopyObjectInto(dst, s)
}

View File

@@ -23,5 +23,7 @@ type CheckTypeSpec struct {
// NewCheckTypeSpec creates a new CheckTypeSpec object.
func NewCheckTypeSpec() *CheckTypeSpec {
return &CheckTypeSpec{}
return &CheckTypeSpec{
Steps: []CheckTypeStep{},
}
}

View File

@@ -3,16 +3,16 @@ package v0alpha1
import "k8s.io/apimachinery/pkg/runtime/schema"
const (
// Group is the API group used by all kinds in this package
Group = "advisor.grafana.app"
// Version is the API version used by all kinds in this package
Version = "v0alpha1"
// APIGroup is the API group used by all kinds in this package
APIGroup = "advisor.grafana.app"
// APIVersion is the API version used by all kinds in this package
APIVersion = "v0alpha1"
)
var (
// GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package
GroupVersion = schema.GroupVersion{
Group: Group,
Version: Version,
Group: APIGroup,
Version: APIVersion,
}
)

View File

@@ -15,6 +15,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.Check": schema_pkg_apis_advisor_v0alpha1_Check(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckErrorLink": schema_pkg_apis_advisor_v0alpha1_CheckErrorLink(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckList": schema_pkg_apis_advisor_v0alpha1_CheckList(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport": schema_pkg_apis_advisor_v0alpha1_CheckReport(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure": schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec": schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckStatus": schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref),
@@ -24,7 +25,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStatus": schema_pkg_apis_advisor_v0alpha1_CheckTypeStatus(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStep": schema_pkg_apis_advisor_v0alpha1_CheckTypeStep(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypestatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckTypestatusOperatorState(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport": schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref),
}
}
@@ -57,8 +57,9 @@ func schema_pkg_apis_advisor_v0alpha1_Check(ref common.ReferenceCallback) common
},
"spec": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec"),
Description: "Spec is the spec of the Check",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec"),
},
},
"status": {
@@ -153,6 +154,43 @@ func schema_pkg_apis_advisor_v0alpha1_CheckList(ref common.ReferenceCallback) co
}
}
func schema_pkg_apis_advisor_v0alpha1_CheckReport(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"count": {
SchemaProps: spec.SchemaProps{
Description: "Number of elements analyzed",
Default: 0,
Type: []string{"integer"},
Format: "int64",
},
},
"failures": {
SchemaProps: spec.SchemaProps{
Description: "List of failures",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"),
},
},
},
},
},
},
Required: []string{"count", "failures"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"},
}
}
func schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -258,7 +296,7 @@ func schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref common.ReferenceCallback)
"report": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport"),
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport"),
},
},
"operatorStates": {
@@ -296,7 +334,7 @@ func schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref common.ReferenceCallback)
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport", "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState"},
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport", "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState"},
}
}
@@ -328,8 +366,9 @@ func schema_pkg_apis_advisor_v0alpha1_CheckType(ref common.ReferenceCallback) co
},
"spec": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec"),
Description: "Spec is the spec of the CheckType",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec"),
},
},
"status": {
@@ -566,43 +605,6 @@ func schema_pkg_apis_advisor_v0alpha1_CheckTypestatusOperatorState(ref common.Re
}
}
func schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"count": {
SchemaProps: spec.SchemaProps{
Description: "Number of elements analyzed",
Default: 0,
Type: []string{"integer"},
Format: "int64",
},
},
"failures": {
SchemaProps: spec.SchemaProps{
Description: "List of failures",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"),
},
},
},
},
},
},
Required: []string{"count", "failures"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"},
}
}
func schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

View File

@@ -7,15 +7,19 @@ package apis
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/resource"
v0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
)
var (
rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`)
rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object"}}`)
versionSchemaCheckv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaCheckv0alpha1, &versionSchemaCheckv0alpha1)
rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}}`)
rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
versionSchemaCheckTypev0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaCheckTypev0alpha1, &versionSchemaCheckTypev0alpha1)
)
@@ -58,12 +62,6 @@ var appManifestData = app.ManifestData{
},
}
func jsonToMap(j string) map[string]any {
m := make(map[string]any)
json.Unmarshal([]byte(j), &j)
return m
}
func LocalManifest() app.Manifest {
return app.NewEmbeddedManifest(appManifestData)
}
@@ -71,3 +69,15 @@ func LocalManifest() app.Manifest {
func RemoteManifest() app.Manifest {
return app.NewAPIServerManifest("advisor")
}
var kindVersionToGoType = map[string]resource.Kind{
"Check/v0alpha1": v0alpha1.CheckKind(),
"CheckType/v0alpha1": v0alpha1.CheckTypeKind(),
}
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
// If there is no association for the provided Kind and Version, exists will return false.
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
return goType, exists
}

View File

@@ -106,3 +106,25 @@ func SetStatusAnnotation(ctx context.Context, client resource.Client, obj resour
}},
}, resource.PatchOptions{}, obj)
}
func SetAnnotations(ctx context.Context, client resource.Client, obj resource.Object, annotations map[string]string) error {
return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{
Operations: []resource.PatchOperation{{
Operation: resource.PatchOpAdd,
Path: "/metadata/annotations",
Value: annotations,
}},
}, resource.PatchOptions{}, obj)
}
func SetStatus(ctx context.Context, client resource.Client, obj resource.Object, status any) error {
return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{
Operations: []resource.PatchOperation{{
Operation: resource.PatchOpAdd,
Path: "/status",
Value: status,
}},
}, resource.PatchOptions{
Subresource: "status",
}, obj)
}

View File

@@ -78,28 +78,21 @@ func processCheck(ctx context.Context, log logging.Logger, client resource.Clien
return fmt.Errorf("error running steps: %w", err)
}
report := &advisorv0alpha1.CheckV0alpha1StatusReport{
report := &advisorv0alpha1.CheckReport{
Failures: failures,
Count: int64(len(items)),
}
c.Status.Report = *report
err = checks.SetStatus(ctx, client, obj, c.Status)
if err != nil {
return err
}
// Set the status annotation to processed and annotate the steps ignored
annotations := checks.AddAnnotations(ctx, obj, map[string]string{
checks.StatusAnnotation: checks.StatusAnnotationProcessed,
checks.IgnoreStepsAnnotationList: checkType.GetAnnotations()[checks.IgnoreStepsAnnotationList],
})
return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{
Operations: []resource.PatchOperation{
{
Operation: resource.PatchOpAdd,
Path: "/status/report",
Value: *report,
}, {
Operation: resource.PatchOpAdd,
Path: "/metadata/annotations",
Value: annotations,
},
},
}, resource.PatchOptions{}, obj)
return checks.SetAnnotations(ctx, client, obj, annotations)
}
func processCheckRetry(ctx context.Context, log logging.Logger, client resource.Client, typesClient resource.Client, obj resource.Object, check checks.Check) error {
@@ -157,7 +150,7 @@ func processCheckRetry(ctx context.Context, log logging.Logger, client resource.
}
}
// Pull failures from the report for the items to retry
c.CheckStatus.Report.Failures = slices.DeleteFunc(c.CheckStatus.Report.Failures, func(f advisorv0alpha1.CheckReportFailure) bool {
c.Status.Report.Failures = slices.DeleteFunc(c.Status.Report.Failures, func(f advisorv0alpha1.CheckReportFailure) bool {
if f.ItemID == itemToRetry {
for _, newFailure := range failures {
if newFailure.StepID == f.StepID {
@@ -171,19 +164,13 @@ func processCheckRetry(ctx context.Context, log logging.Logger, client resource.
// Failure not in the list of items to retry, keep it
return false
})
err = checks.SetStatus(ctx, client, obj, c.Status)
if err != nil {
return err
}
// Delete the retry annotation to mark the check as processed
annotations := checks.DeleteAnnotations(ctx, obj, []string{checks.RetryAnnotation})
return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{
Operations: []resource.PatchOperation{{
Operation: resource.PatchOpAdd,
Path: "/status/report",
Value: c.CheckStatus.Report,
}, {
Operation: resource.PatchOpAdd,
Path: "/metadata/annotations",
Value: annotations,
}},
}, resource.PatchOptions{}, obj)
return checks.SetAnnotations(ctx, client, obj, annotations)
}
func runStepsInParallel(ctx context.Context, log logging.Logger, spec *advisorv0alpha1.CheckSpec, steps []checks.Step, items []any) ([]advisorv0alpha1.CheckReportFailure, error) {

View File

@@ -95,9 +95,9 @@ func TestProcessMultipleCheckItems(t *testing.T) {
err = processCheck(ctx, logging.DefaultLogger, client, typesClient, obj, check)
assert.NoError(t, err)
assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation])
r := client.lastValue.(advisorv0alpha1.CheckV0alpha1StatusReport)
assert.Equal(t, r.Count, int64(100))
assert.Len(t, r.Failures, 50)
r := client.values[0].(advisorv0alpha1.CheckStatus)
assert.Equal(t, r.Report.Count, int64(100))
assert.Len(t, r.Report.Failures, 50)
}
func TestProcessCheck_AlreadyProcessed(t *testing.T) {
@@ -231,7 +231,7 @@ func TestProcessCheckRetry_SkipMissingItem(t *testing.T) {
checks.RetryAnnotation: "item",
checks.StatusAnnotation: checks.StatusAnnotationProcessed,
})
obj.CheckStatus.Report.Failures = []advisorv0alpha1.CheckReportFailure{
obj.Status.Report.Failures = []advisorv0alpha1.CheckReportFailure{
{
ItemID: "item",
StepID: "step",
@@ -254,7 +254,7 @@ func TestProcessCheckRetry_SkipMissingItem(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation])
assert.Empty(t, obj.GetAnnotations()[checks.RetryAnnotation])
assert.Empty(t, obj.CheckStatus.Report.Failures)
assert.Empty(t, obj.Status.Report.Failures)
}
func TestProcessCheckRetry_Success(t *testing.T) {
@@ -263,7 +263,7 @@ func TestProcessCheckRetry_Success(t *testing.T) {
checks.RetryAnnotation: "item",
checks.StatusAnnotation: checks.StatusAnnotationProcessed,
})
obj.CheckStatus.Report.Failures = []advisorv0alpha1.CheckReportFailure{
obj.Status.Report.Failures = []advisorv0alpha1.CheckReportFailure{
{
ItemID: "item",
StepID: "step",
@@ -286,16 +286,16 @@ func TestProcessCheckRetry_Success(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation])
assert.Empty(t, obj.GetAnnotations()[checks.RetryAnnotation])
assert.Empty(t, obj.CheckStatus.Report.Failures)
assert.Empty(t, obj.Status.Report.Failures)
}
type mockClient struct {
resource.Client
lastValue any
values []any
}
func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions, obj resource.Object) error {
m.lastValue = req.Operations[0].Value
m.values = append(m.values, req.Operations[0].Value)
return nil
}

View File

@@ -10,6 +10,7 @@ RoleSpec: {
// Display name of the role
title: string
description: string
version: int
group: string

View File

@@ -18,9 +18,10 @@ func NewCoreRolespecPermission() *CoreRolespecPermission {
// +k8s:openapi-gen=true
type CoreRoleSpec struct {
// Display name of the role
Title string `json:"title"`
Version int64 `json:"version"`
Group string `json:"group"`
Title string `json:"title"`
Description string `json:"description"`
Version int64 `json:"version"`
Group string `json:"group"`
// TODO:
// delegatable?: bool
// created?

View File

@@ -18,9 +18,10 @@ func NewGlobalRolespecPermission() *GlobalRolespecPermission {
// +k8s:openapi-gen=true
type GlobalRoleSpec struct {
// Display name of the role
Title string `json:"title"`
Version int64 `json:"version"`
Group string `json:"group"`
Title string `json:"title"`
Description string `json:"description"`
Version int64 `json:"version"`
Group string `json:"group"`
// TODO:
// delegatable?: bool
// created?

View File

@@ -18,9 +18,10 @@ func NewRolespecPermission() *RolespecPermission {
// +k8s:openapi-gen=true
type RoleSpec struct {
// Display name of the role
Title string `json:"title"`
Version int64 `json:"version"`
Group string `json:"group"`
Title string `json:"title"`
Description string `json:"description"`
Version int64 `json:"version"`
Group string `json:"group"`
// TODO:
// delegatable?: bool
// created?

View File

@@ -186,6 +186,13 @@ func schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref common.ReferenceCallback) com
Format: "",
},
},
"description": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"version": {
SchemaProps: spec.SchemaProps{
Default: 0,
@@ -215,7 +222,7 @@ func schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref common.ReferenceCallback) com
},
},
},
Required: []string{"title", "version", "group", "permissions"},
Required: []string{"title", "description", "version", "group", "permissions"},
},
},
Dependencies: []string{
@@ -740,6 +747,13 @@ func schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref common.ReferenceCallback) c
Format: "",
},
},
"description": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"version": {
SchemaProps: spec.SchemaProps{
Default: 0,
@@ -769,7 +783,7 @@ func schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref common.ReferenceCallback) c
},
},
},
Required: []string{"title", "version", "group", "permissions"},
Required: []string{"title", "description", "version", "group", "permissions"},
},
},
Dependencies: []string{
@@ -1600,6 +1614,13 @@ func schema_pkg_apis_iam_v0alpha1_RoleSpec(ref common.ReferenceCallback) common.
Format: "",
},
},
"description": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"version": {
SchemaProps: spec.SchemaProps{
Default: 0,
@@ -1629,7 +1650,7 @@ func schema_pkg_apis_iam_v0alpha1_RoleSpec(ref common.ReferenceCallback) common.
},
},
},
Required: []string{"title", "version", "group", "permissions"},
Required: []string{"title", "description", "version", "group", "permissions"},
},
},
Dependencies: []string{

View File

@@ -62,7 +62,7 @@ The following guides will help you get started with Loki:
This data source supports these versions of Loki:
- v2.8+
- v2.9+
## Adding a data source

View File

@@ -72,7 +72,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `pluginsSriChecks` | Enables SRI checks for plugin assets | |
| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | |
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes |
| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | Yes |
| `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes |
| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes |
| `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes |

13
go.mod
View File

@@ -58,8 +58,6 @@ require (
github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group
github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage
github.com/getkin/kin-openapi v0.132.0 // @grafana/grafana-app-platform-squad
github.com/go-git/go-billy/v5 v5.6.2 // @grafana/grafana-app-platform-squad
github.com/go-git/go-git/v5 v5.14.0 // @grafana/grafana-app-platform-squad
github.com/go-jose/go-jose/v3 v3.0.4 // @grafana/identity-access-team
github.com/go-jose/go-jose/v4 v4.1.0 // indirect; @grafana/identity-access-team
github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group
@@ -87,7 +85,7 @@ require (
github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945 // @grafana/alerting-backend
github.com/grafana/alerting v0.0.0-20250709204613-c5c6f9c1653d // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
@@ -276,7 +274,6 @@ require (
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/RoaringBitmap/roaring v1.9.3 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
@@ -348,7 +345,6 @@ require (
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
@@ -372,7 +368,6 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gammazero/deque v0.2.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
@@ -423,7 +418,6 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jaegertracing/jaeger-idl v0.5.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
@@ -437,7 +431,6 @@ require (
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/asmfmt v1.3.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
@@ -492,7 +485,6 @@ require (
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
@@ -519,7 +511,6 @@ require (
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sony/gobreaker v0.5.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
@@ -538,7 +529,6 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
github.com/yudai/pp v2.0.1+incompatible // indirect
@@ -586,7 +576,6 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect
gopkg.in/telebot.v3 v3.2.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
k8s.io/apiextensions-apiserver v0.33.2 // indirect
k8s.io/kms v0.33.2 // indirect
modernc.org/libc v1.65.0 // indirect

32
go.sum
View File

@@ -744,7 +744,6 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
@@ -803,8 +802,6 @@ github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqR
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
@@ -1054,8 +1051,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
@@ -1200,8 +1195,6 @@ github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7M
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
@@ -1211,14 +1204,6 @@ github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -1586,8 +1571,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945 h1:3imTbxFpZSVI6IBIB9mn+Xc40lUweWjfMaBSgXR7rLs=
github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945/go.mod h1:gtR7agmxVfJOmNKV/n2ZULgOYTYNL+PDKYB5N48tQ7Q=
github.com/grafana/alerting v0.0.0-20250709204613-c5c6f9c1653d h1:rtlYpwsE3KDWWCg2kytDw3s5qgpDjG87qh1IixAyNz4=
github.com/grafana/alerting v0.0.0-20250709204613-c5c6f9c1653d/go.mod h1:gtR7agmxVfJOmNKV/n2ZULgOYTYNL+PDKYB5N48tQ7Q=
github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed h1:k5Ng33zE9fCawqfEVybOasXY7/FQD5Qg2J92ePneeVM=
github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
@@ -1815,8 +1800,6 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE=
github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
@@ -1877,8 +1860,6 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -2175,8 +2156,6 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -2352,11 +2331,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
@@ -2481,8 +2457,6 @@ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+x
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
@@ -3542,8 +3516,6 @@ gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs=
gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -201,7 +201,7 @@
"expose-loader": "5.0.1",
"fishery": "^2.2.2",
"fork-ts-checker-webpack-plugin": "9.0.2",
"glob": "11.0.1",
"glob": "11.0.3",
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.3",
"http-server": "14.1.1",
@@ -217,7 +217,7 @@
"jest-watch-typeahead": "^2.2.2",
"jimp": "^1.6.0",
"jsdom-testing-mocks": "^1.13.1",
"lerna": "8.2.1",
"lerna": "8.2.3",
"mini-css-extract-plugin": "2.9.2",
"msw": "2.10.3",
"mutationobserver-shim": "0.3.7",
@@ -227,7 +227,7 @@
"pa11y-ci": "^3.1.0",
"pdf-parse": "^1.1.1",
"plop": "^4.0.1",
"postcss": "8.5.1",
"postcss": "8.5.6",
"postcss-loader": "8.1.1",
"postcss-reporter": "7.1.0",
"postcss-scss": "4.0.9",

View File

@@ -34,6 +34,11 @@ export interface TransformerRegistryItem<TOptions = any> extends RegistryItem {
* Set of categories associated with the transformer
*/
categories?: Set<TransformerCategory>;
/**
* Set of tags associated with the transformer for improved transformation search
*/
tags?: Set<string>;
}
export enum TransformerCategory {

View File

@@ -210,10 +210,6 @@ export interface FeatureToggles {
*/
provisioning?: boolean;
/**
* Use experimental git library for provisioning
*/
nanoGit?: boolean;
/**
* Start an additional https handler and write kubectl options
*/
grafanaAPIServerEnsureKubectlAccess?: boolean;
@@ -738,11 +734,6 @@ export interface FeatureToggles {
*/
crashDetection?: boolean;
/**
* Enables querying the Jaeger data source without the proxy
* @default true
*/
jaegerBackendMigration?: boolean;
/**
* Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query
* @default true
*/

View File

@@ -9,7 +9,7 @@ This is a Flamegraph component that is used in Grafana and Pyroscope web app to
Currently this library exposes single component `Flamegraph` that renders whole visualization used for profiling which contains a header, a table representation of the data and a flamegraph.
```tsx
import { Flamegraph } from '@grafana/flamegraph';
import { FlameGraph } from '@grafana/flamegraph';
<FlameGraph
getTheme={() => createTheme({ colors: { mode: 'dark' } })}

View File

@@ -16,7 +16,7 @@
"eslint": "9.19.0",
"eslint-webpack-plugin": "4.2.0",
"fork-ts-checker-webpack-plugin": "9.0.2",
"glob": "11.0.1",
"glob": "11.0.3",
"imports-loader": "^5.0.0",
"replace-in-file-webpack-plugin": "1.0.6",
"swc-loader": "0.2.6",

View File

@@ -36,8 +36,22 @@ const (
serviceNameForProvisioning = "provisioning"
)
func newInternalIdentity(name string, namespace string, orgID int64) Requester {
return &StaticRequester{
type IdentityOpts func(*StaticRequester)
// WithServiceIdentityName sets the `StaticRequester.AccessTokenClaims.Rest.ServiceIdentity` field to the provided name.
// This is so far only used by Secrets Manager to identify and gate the service decrypting a secret.
func WithServiceIdentityName(name string) IdentityOpts {
return func(r *StaticRequester) {
r.AccessTokenClaims.Rest.ServiceIdentity = name
}
}
func newInternalIdentity(name string, namespace string, orgID int64, opts ...IdentityOpts) Requester {
// Create a copy of the ServiceIdentityClaims to avoid modifying the global one.
// Some of the options might mutate it.
claimsCopy := *ServiceIdentityClaims
staticRequester := &StaticRequester{
Type: types.TypeAccessPolicy,
Name: name,
UserUID: name,
@@ -50,37 +64,43 @@ func newInternalIdentity(name string, namespace string, orgID int64) Requester {
Permissions: map[int64]map[string][]string{
orgID: serviceIdentityPermissions,
},
AccessTokenClaims: ServiceIdentityClaims,
AccessTokenClaims: &claimsCopy,
}
for _, opt := range opts {
opt(staticRequester)
}
return staticRequester
}
// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context.
// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with
// static permissions so it can be used in legacy code paths.
func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) {
r := newInternalIdentity(serviceName, "*", orgID)
func WithServiceIdentity(ctx context.Context, orgID int64, opts ...IdentityOpts) (context.Context, Requester) {
r := newInternalIdentity(serviceName, "*", orgID, opts...)
return WithRequester(ctx, r), r
}
func WithProvisioningIdentity(ctx context.Context, namespace string) (context.Context, Requester, error) {
func WithProvisioningIdentity(ctx context.Context, namespace string, opts ...IdentityOpts) (context.Context, Requester, error) {
ns, err := types.ParseNamespace(namespace)
if err != nil {
return nil, nil, err
}
r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID)
r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID, opts...)
return WithRequester(ctx, r), r, nil
}
// WithServiceIdentityContext sets an identity representing the service itself in context.
func WithServiceIdentityContext(ctx context.Context, orgID int64) context.Context {
ctx, _ = WithServiceIdentity(ctx, orgID)
func WithServiceIdentityContext(ctx context.Context, orgID int64, opts ...IdentityOpts) context.Context {
ctx, _ = WithServiceIdentity(ctx, orgID, opts...)
return ctx
}
// WithServiceIdentityFN calls provided closure with an context contaning the identity of the service.
func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error)) (T, error) {
return fn(WithServiceIdentityContext(ctx, orgID))
func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error), opts ...IdentityOpts) (T, error) {
return fn(WithServiceIdentityContext(ctx, orgID, opts...))
}
func getWildcardPermissions(actions ...string) map[string][]string {
@@ -91,14 +111,6 @@ func getWildcardPermissions(actions ...string) map[string][]string {
return permissions
}
func getTokenPermissions(groups ...string) []string {
out := make([]string, 0, len(groups))
for _, group := range groups {
out = append(out, group+":*")
}
return out
}
// serviceIdentityPermissions is a list of wildcard permissions for provided actions.
// We should add every action required "internally" here.
var serviceIdentityPermissions = getWildcardPermissions(
@@ -121,13 +133,16 @@ var serviceIdentityPermissions = getWildcardPermissions(
"serviceaccounts:read", // serviceaccounts.ActionRead,
)
var serviceIdentityTokenPermissions = getTokenPermissions(
"folder.grafana.app",
"dashboard.grafana.app",
"secret.grafana.app",
"query.grafana.app",
"iam.grafana.app",
)
var serviceIdentityTokenPermissions = []string{
"folder.grafana.app:*",
"dashboard.grafana.app:*",
"secret.grafana.app:*",
"query.grafana.app:*",
"iam.grafana.app:*",
// Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions.
"secret.grafana.app/securevalues:decrypt",
}
var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{
Rest: authn.AccessTokenClaims{

View File

@@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/grafana/authlib/authn"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@@ -24,3 +25,48 @@ func TestRequesterFromContext(t *testing.T) {
require.Equal(t, expected.GetUID(), actual.GetUID())
})
}
func TestWithServiceIdentity(t *testing.T) {
t.Run("with a custom service identity name", func(t *testing.T) {
customName := "custom-service"
orgID := int64(1)
ctx, requester := identity.WithServiceIdentity(context.Background(), orgID, identity.WithServiceIdentityName(customName))
require.NotNil(t, requester)
require.Equal(t, orgID, requester.GetOrgID())
require.Equal(t, customName, requester.GetExtra()[string(authn.ServiceIdentityKey)][0])
require.Contains(t, requester.GetTokenPermissions(), "secret.grafana.app/securevalues:decrypt")
fromCtx, err := identity.GetRequester(ctx)
require.NoError(t, err)
require.Equal(t, customName, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)][0])
// Reuse the context but create another identity on top with a different name and org ID
anotherCustomName := "another-custom-service"
anotherOrgID := int64(2)
ctx2 := identity.WithServiceIdentityContext(ctx, anotherOrgID, identity.WithServiceIdentityName(anotherCustomName))
fromCtx, err = identity.GetRequester(ctx2)
require.NoError(t, err)
require.Equal(t, anotherOrgID, fromCtx.GetOrgID())
require.Equal(t, anotherCustomName, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)][0])
// Reuse the context but create another identity without a custom name
ctx3, requester := identity.WithServiceIdentity(ctx2, 1)
require.NotNil(t, requester)
require.Empty(t, requester.GetExtra()[string(authn.ServiceIdentityKey)])
fromCtx, err = identity.GetRequester(ctx3)
require.NoError(t, err)
require.Empty(t, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)])
})
t.Run("without a custom service identity name", func(t *testing.T) {
ctx, requester := identity.WithServiceIdentity(context.Background(), 1)
require.NotNil(t, requester)
require.Empty(t, requester.GetExtra()[string(authn.ServiceIdentityKey)])
fromCtx, err := identity.GetRequester(ctx)
require.NoError(t, err)
require.Empty(t, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)])
})
}

View File

@@ -128,12 +128,6 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime.
return
}
// TODO: Implement folder delete
if r.Method == http.MethodDelete && isDir {
responder.Error(apierrors.NewBadRequest("folder navigation not yet supported"))
return
}
var obj *provisioning.ResourceWrapper
code := http.StatusOK
switch r.Method {

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package export

View File

@@ -1,87 +0,0 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
package export
import (
context "context"
repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
mock "github.com/stretchr/testify/mock"
)
// MockWrapWithCloneFn is an autogenerated mock type for the WrapWithCloneFn type
type MockWrapWithCloneFn struct {
mock.Mock
}
type MockWrapWithCloneFn_Expecter struct {
mock *mock.Mock
}
func (_m *MockWrapWithCloneFn) EXPECT() *MockWrapWithCloneFn_Expecter {
return &MockWrapWithCloneFn_Expecter{mock: &_m.Mock}
}
// Execute provides a mock function with given fields: ctx, repo, cloneOptions, pushOptions, fn
func (_m *MockWrapWithCloneFn) Execute(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error) error {
ret := _m.Called(ctx, repo, cloneOptions, pushOptions, fn)
if len(ret) == 0 {
panic("no return value specified for Execute")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error); ok {
r0 = rf(ctx, repo, cloneOptions, pushOptions, fn)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockWrapWithCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute'
type MockWrapWithCloneFn_Execute_Call struct {
*mock.Call
}
// Execute is a helper method to define mock.On call
// - ctx context.Context
// - repo repository.Repository
// - cloneOptions repository.CloneOptions
// - pushOptions repository.PushOptions
// - fn func(repository.Repository , bool) error
func (_e *MockWrapWithCloneFn_Expecter) Execute(ctx interface{}, repo interface{}, cloneOptions interface{}, pushOptions interface{}, fn interface{}) *MockWrapWithCloneFn_Execute_Call {
return &MockWrapWithCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, cloneOptions, pushOptions, fn)}
}
func (_c *MockWrapWithCloneFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error)) *MockWrapWithCloneFn_Execute_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.CloneOptions), args[3].(repository.PushOptions), args[4].(func(repository.Repository, bool) error))
})
return _c
}
func (_c *MockWrapWithCloneFn_Execute_Call) Return(_a0 error) *MockWrapWithCloneFn_Execute_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWrapWithCloneFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error) *MockWrapWithCloneFn_Execute_Call {
_c.Call.Return(run)
return _c
}
// NewMockWrapWithCloneFn creates a new instance of MockWrapWithCloneFn. 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 NewMockWrapWithCloneFn(t interface {
mock.TestingT
Cleanup(func())
}) *MockWrapWithCloneFn {
mock := &MockWrapWithCloneFn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -0,0 +1,86 @@
// Code generated by mockery v2.52.4. DO NOT EDIT.
package export
import (
context "context"
repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
mock "github.com/stretchr/testify/mock"
)
// MockWrapWithStageFn is an autogenerated mock type for the WrapWithStageFn type
type MockWrapWithStageFn struct {
mock.Mock
}
type MockWrapWithStageFn_Expecter struct {
mock *mock.Mock
}
func (_m *MockWrapWithStageFn) EXPECT() *MockWrapWithStageFn_Expecter {
return &MockWrapWithStageFn_Expecter{mock: &_m.Mock}
}
// Execute provides a mock function with given fields: ctx, repo, stageOptions, fn
func (_m *MockWrapWithStageFn) Execute(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
ret := _m.Called(ctx, repo, stageOptions, fn)
if len(ret) == 0 {
panic("no return value specified for Execute")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.StageOptions, func(repository.Repository, bool) error) error); ok {
r0 = rf(ctx, repo, stageOptions, fn)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockWrapWithStageFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute'
type MockWrapWithStageFn_Execute_Call struct {
*mock.Call
}
// Execute is a helper method to define mock.On call
// - ctx context.Context
// - repo repository.Repository
// - stageOptions repository.StageOptions
// - fn func(repository.Repository , bool) error
func (_e *MockWrapWithStageFn_Expecter) Execute(ctx interface{}, repo interface{}, stageOptions interface{}, fn interface{}) *MockWrapWithStageFn_Execute_Call {
return &MockWrapWithStageFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, stageOptions, fn)}
}
func (_c *MockWrapWithStageFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error)) *MockWrapWithStageFn_Execute_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.StageOptions), args[3].(func(repository.Repository, bool) error))
})
return _c
}
func (_c *MockWrapWithStageFn_Execute_Call) Return(_a0 error) *MockWrapWithStageFn_Execute_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWrapWithStageFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.StageOptions, func(repository.Repository, bool) error) error) *MockWrapWithStageFn_Execute_Call {
_c.Call.Return(run)
return _c
}
// NewMockWrapWithStageFn creates a new instance of MockWrapWithStageFn. 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 NewMockWrapWithStageFn(t interface {
mock.TestingT
Cleanup(func())
}) *MockWrapWithStageFn {
mock := &MockWrapWithStageFn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -67,7 +67,7 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions,
}
}
if err := exportResource(ctx, options, client, shim, repositoryResources, progress); err != nil {
if err := exportResource(ctx, kind.Resource, options, client, shim, repositoryResources, progress); err != nil {
return fmt.Errorf("export %s: %w", kind.Resource, err)
}
}
@@ -76,6 +76,7 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions,
}
func exportResource(ctx context.Context,
resource string,
options provisioning.ExportJobOptions,
client dynamic.ResourceInterface,
shim conversionShim,
@@ -88,7 +89,7 @@ func exportResource(ctx context.Context,
gvk := item.GroupVersionKind()
result := jobs.JobResourceResult{
Name: item.GetName(),
Resource: gvk.Kind,
Resource: resource,
Group: gvk.Group,
Action: repository.FileActionCreated,
}

View File

@@ -9,34 +9,33 @@ import (
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
)
//go:generate mockery --name ExportFn --structname MockExportFn --inpackage --filename mock_export_fn.go --with-expecter
type ExportFn func(ctx context.Context, repoName string, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error
//go:generate mockery --name WrapWithCloneFn --structname MockWrapWithCloneFn --inpackage --filename mock_wrap_with_clone_fn.go --with-expecter
type WrapWithCloneFn func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repo repository.Repository, cloned bool) error) error
//go:generate mockery --name WrapWithStageFn --structname MockWrapWithStageFn --inpackage --filename mock_wrap_with_stage_fn.go --with-expecter
type WrapWithStageFn func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repo repository.Repository, staged bool) error) error
type ExportWorker struct {
clientFactory resources.ClientFactory
repositoryResources resources.RepositoryResourcesFactory
exportFn ExportFn
wrapWithCloneFn WrapWithCloneFn
wrapWithStageFn WrapWithStageFn
}
func NewExportWorker(
clientFactory resources.ClientFactory,
repositoryResources resources.RepositoryResourcesFactory,
exportFn ExportFn,
wrapWithCloneFn WrapWithCloneFn,
wrapWithStageFn WrapWithStageFn,
) *ExportWorker {
return &ExportWorker{
clientFactory: clientFactory,
repositoryResources: repositoryResources,
exportFn: exportFn,
wrapWithCloneFn: wrapWithCloneFn,
wrapWithStageFn: wrapWithStageFn,
}
}
@@ -57,32 +56,9 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository,
return err
}
writer := gogit.Progress(func(line string) {
progress.SetMessage(ctx, line)
}, "finished")
cloneOptions := repository.CloneOptions{
cloneOptions := repository.StageOptions{
Timeout: 10 * time.Minute,
PushOnWrites: false,
Progress: writer,
BeforeFn: func() error {
progress.SetMessage(ctx, "clone target")
// :( the branch is now baked into the repo
if options.Branch != "" {
return fmt.Errorf("branch is not supported for clonable repositories")
}
return nil
},
}
pushOptions := repository.PushOptions{
Timeout: 10 * time.Minute,
Progress: writer,
BeforeFn: func() error {
progress.SetMessage(ctx, "push changes")
return nil
},
}
fn := func(repo repository.Repository, _ bool) error {
@@ -104,5 +80,5 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository,
return r.exportFn(ctx, cfg.Name, *options, clients, repositoryResources, progress)
}
return r.wrapWithCloneFn(ctx, repo, cloneOptions, pushOptions, fn)
return r.wrapWithStageFn(ctx, repo, cloneOptions, fn)
}

View File

@@ -7,7 +7,6 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -143,12 +142,12 @@ func TestExportWorker_ProcessFailedToCreateClients(t *testing.T) {
mockClients := resources.NewMockClientFactory(t)
mockClients.On("Clients", context.Background(), "test-namespace").Return(nil, errors.New("failed to create clients"))
mockCloneFn := NewMockWrapWithCloneFn(t)
mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
mockStageFn := NewMockWrapWithStageFn(t)
mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(repo, true)
})
r := NewExportWorker(mockClients, nil, nil, mockCloneFn.Execute)
r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute)
mockProgress := jobs.NewMockJobProgressRecorder(t)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
@@ -179,12 +178,12 @@ func TestExportWorker_ProcessNotReaderWriter(t *testing.T) {
mockClients.On("Clients", context.Background(), "test-namespace").Return(resourceClients, nil)
mockProgress := jobs.NewMockJobProgressRecorder(t)
mockCloneFn := NewMockWrapWithCloneFn(t)
mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
mockStageFn := NewMockWrapWithStageFn(t)
mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(repo, true)
})
r := NewExportWorker(mockClients, nil, nil, mockCloneFn.Execute)
r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.EqualError(t, err, "export job submitted targeting repository that is not a ReaderWriter")
}
@@ -216,16 +215,16 @@ func TestExportWorker_ProcessRepositoryResourcesError(t *testing.T) {
mockRepoResources.On("Client", context.Background(), mockRepo).Return(nil, fmt.Errorf("failed to create repository resources client"))
mockProgress := jobs.NewMockJobProgressRecorder(t)
mockCloneFn := NewMockWrapWithCloneFn(t)
mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
mockStageFn := NewMockWrapWithStageFn(t)
mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(repo, true)
})
r := NewExportWorker(mockClients, mockRepoResources, nil, mockCloneFn.Execute)
r := NewExportWorker(mockClients, mockRepoResources, nil, mockStageFn.Execute)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.EqualError(t, err, "create repository resource client: failed to create repository resources client")
}
func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) {
func TestExportWorker_ProcessStageOptions(t *testing.T) {
job := v0alpha1.Job{
Spec: v0alpha1.JobSpec{
Action: v0alpha1.JobActionPush,
@@ -245,9 +244,7 @@ func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) {
})
mockProgress := jobs.NewMockJobProgressRecorder(t)
// Verify progress messages are set
mockProgress.On("SetMessage", mock.Anything, "clone target").Return()
mockProgress.On("SetMessage", mock.Anything, "push changes").Return()
// No progress messages expected in current implementation
mockClients := resources.NewMockClientFactory(t)
mockResourceClients := resources.NewMockResourceClients(t)
@@ -260,21 +257,15 @@ func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) {
mockExportFn := NewMockExportFn(t)
mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
mockCloneFn := NewMockWrapWithCloneFn(t)
mockStageFn := NewMockWrapWithStageFn(t)
// Verify clone and push options
mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.CloneOptions) bool {
return opts.Timeout == 10*time.Minute && !opts.PushOnWrites && opts.BeforeFn != nil
}), mock.MatchedBy(func(opts repository.PushOptions) bool {
return opts.Timeout == 10*time.Minute && opts.Progress != nil && opts.BeforeFn != nil
}), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
// Execute both BeforeFn functions to verify progress messages
assert.NoError(t, cloneOpts.BeforeFn())
assert.NoError(t, pushOpts.BeforeFn())
mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool {
return opts.Timeout == 10*time.Minute && !opts.PushOnWrites
}), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(repo, true)
})
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute)
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.NoError(t, err)
}
@@ -310,17 +301,17 @@ func TestExportWorker_ProcessExportFnError(t *testing.T) {
mockExportFn := NewMockExportFn(t)
mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("export failed"))
mockCloneFn := NewMockWrapWithCloneFn(t)
mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
mockStageFn := NewMockWrapWithStageFn(t)
mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(repo, true)
})
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute)
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.EqualError(t, err, "export failed")
}
func TestExportWorker_ProcessWrapWithCloneFnError(t *testing.T) {
func TestExportWorker_ProcessWrapWithStageFnError(t *testing.T) {
job := v0alpha1.Job{
Spec: v0alpha1.JobSpec{
Action: v0alpha1.JobActionPush,
@@ -340,15 +331,15 @@ func TestExportWorker_ProcessWrapWithCloneFnError(t *testing.T) {
})
mockProgress := jobs.NewMockJobProgressRecorder(t)
mockCloneFn := NewMockWrapWithCloneFn(t)
mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("clone failed"))
mockStageFn := NewMockWrapWithStageFn(t)
mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(errors.New("stage failed"))
r := NewExportWorker(nil, nil, nil, mockCloneFn.Execute)
r := NewExportWorker(nil, nil, nil, mockStageFn.Execute)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.EqualError(t, err, "clone failed")
require.EqualError(t, err, "stage failed")
}
func TestExportWorker_ProcessBranchNotAllowedForClonableRepositories(t *testing.T) {
func TestExportWorker_ProcessBranchNotAllowedForStageableRepositories(t *testing.T) {
job := v0alpha1.Job{
Spec: v0alpha1.JobSpec{
Action: v0alpha1.JobActionPush,
@@ -362,24 +353,16 @@ func TestExportWorker_ProcessBranchNotAllowedForClonableRepositories(t *testing.
mockRepo.On("Config").Return(&v0alpha1.Repository{
Spec: v0alpha1.RepositorySpec{
Type: v0alpha1.GitHubRepositoryType,
Workflows: []v0alpha1.Workflow{v0alpha1.BranchWorkflow},
Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow}, // Only write workflow, not branch
},
})
mockProgress := jobs.NewMockJobProgressRecorder(t)
mockProgress.On("SetMessage", mock.Anything, "clone target").Return()
mockCloneFn := NewMockWrapWithCloneFn(t)
mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
if cloneOpts.BeforeFn != nil {
return cloneOpts.BeforeFn()
}
// No progress messages expected in current implementation
return fn(repo, true)
})
r := NewExportWorker(nil, nil, nil, mockCloneFn.Execute)
r := NewExportWorker(nil, nil, nil, nil)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.EqualError(t, err, "branch is not supported for clonable repositories")
require.EqualError(t, err, "this repository does not support the branch workflow")
}
func TestExportWorker_ProcessGitRepository(t *testing.T) {
@@ -407,9 +390,7 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) {
})
mockProgress := jobs.NewMockJobProgressRecorder(t)
// Verify progress messages are set
mockProgress.On("SetMessage", mock.Anything, "clone target").Return()
mockProgress.On("SetMessage", mock.Anything, "push changes").Return()
// No progress messages expected in current implementation
mockClients := resources.NewMockClientFactory(t)
mockResourceClients := resources.NewMockResourceClients(t)
@@ -422,21 +403,15 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) {
mockExportFn := NewMockExportFn(t)
mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
mockCloneFn := NewMockWrapWithCloneFn(t)
mockStageFn := NewMockWrapWithStageFn(t)
// Verify clone and push options
mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.CloneOptions) bool {
return opts.Timeout == 10*time.Minute && !opts.PushOnWrites && opts.BeforeFn != nil
}), mock.MatchedBy(func(opts repository.PushOptions) bool {
return opts.Timeout == 10*time.Minute && opts.Progress != nil && opts.BeforeFn != nil
}), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
// Execute both BeforeFn functions to verify progress messages
assert.NoError(t, cloneOpts.BeforeFn())
assert.NoError(t, pushOpts.BeforeFn())
mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool {
return opts.Timeout == 10*time.Minute && !opts.PushOnWrites
}), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(repo, true)
})
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute)
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.NoError(t, err)
}
@@ -477,12 +452,12 @@ func TestExportWorker_ProcessGitRepositoryExportFnError(t *testing.T) {
mockExportFn := NewMockExportFn(t)
mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("export failed"))
mockCloneFn := NewMockWrapWithCloneFn(t)
mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
mockStageFn := NewMockWrapWithStageFn(t)
mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(repo, true)
})
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute)
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.EqualError(t, err, "export failed")
}

View File

@@ -10,57 +10,38 @@ import (
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git"
)
type LegacyMigrator struct {
legacyMigrator LegacyResourcesMigrator
storageSwapper StorageSwapper
syncWorker jobs.Worker
wrapWithCloneFn WrapWithCloneFn
wrapWithStageFn WrapWithStageFn
}
func NewLegacyMigrator(
legacyMigrator LegacyResourcesMigrator,
storageSwapper StorageSwapper,
syncWorker jobs.Worker,
wrapWithCloneFn WrapWithCloneFn,
wrapWithStageFn WrapWithStageFn,
) *LegacyMigrator {
return &LegacyMigrator{
legacyMigrator: legacyMigrator,
storageSwapper: storageSwapper,
syncWorker: syncWorker,
wrapWithCloneFn: wrapWithCloneFn,
wrapWithStageFn: wrapWithStageFn,
}
}
func (m *LegacyMigrator) Migrate(ctx context.Context, rw repository.ReaderWriter, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error {
namespace := rw.Config().Namespace
writer := gogit.Progress(func(line string) {
progress.SetMessage(ctx, line)
}, "finished")
cloneOptions := repository.CloneOptions{
stageOptions := repository.StageOptions{
PushOnWrites: options.History,
// TODO: make this configurable
Timeout: 10 * time.Minute,
Progress: writer,
BeforeFn: func() error {
progress.SetMessage(ctx, "clone repository")
return nil
},
}
pushOptions := repository.PushOptions{
// TODO: make this configurable
Timeout: 10 * time.Minute,
Progress: writer,
BeforeFn: func() error {
progress.SetMessage(ctx, "push changes")
return nil
},
Timeout: 10 * time.Minute,
}
if err := m.wrapWithCloneFn(ctx, rw, cloneOptions, pushOptions, func(repo repository.Repository, cloned bool) error {
if err := m.wrapWithStageFn(ctx, rw, stageOptions, func(repo repository.Repository, staged bool) error {
rw, ok := repo.(repository.ReaderWriter)
if !ok {
return errors.New("migration job submitted targeting repository that is not a ReaderWriter")

View File

@@ -3,7 +3,6 @@ package migrate
import (
"context"
"errors"
"fmt"
"testing"
"time"
@@ -16,12 +15,12 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
)
func TestWrapWithCloneFn(t *testing.T) {
func TestWrapWithStageFn(t *testing.T) {
t.Run("should return error when repository is not a ReaderWriter", func(t *testing.T) {
// Setup
ctx := context.Background()
// Create the wrapper function that matches WrapWithCloneFn signature
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
// pass a reader to function call
repo := repository.NewMockReader(t)
return fn(repo, true)
@@ -56,7 +55,7 @@ func TestWrapWithCloneFn_Error(t *testing.T) {
expectedErr := errors.New("clone failed")
// Create the wrapper function that returns an error
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return expectedErr
}
@@ -98,7 +97,7 @@ func TestLegacyMigrator_MigrateFails(t *testing.T) {
mockWorker := jobs.NewMockWorker(t)
// Create a wrapper function that calls the provided function
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(rw, true)
}
@@ -147,7 +146,7 @@ func TestLegacyMigrator_ResetUnifiedStorageFails(t *testing.T) {
mockWorker := jobs.NewMockWorker(t)
// Create a wrapper function that calls the provided function
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(rw, true)
}
@@ -202,7 +201,7 @@ func TestLegacyMigrator_SyncFails(t *testing.T) {
}), mock.Anything).Return(expectedErr)
// Create a wrapper function that calls the provided function
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(rw, true)
}
@@ -257,7 +256,7 @@ func TestLegacyMigrator_SyncFails(t *testing.T) {
}), mock.Anything).Return(syncErr)
// Create a wrapper function that calls the provided function
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(rw, true)
}
@@ -310,7 +309,7 @@ func TestLegacyMigrator_Success(t *testing.T) {
}), mock.Anything).Return(nil)
// Create a wrapper function that calls the provided function
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return fn(rw, true)
}
@@ -352,19 +351,7 @@ func TestLegacyMigrator_BeforeFnExecution(t *testing.T) {
mockStorageSwapper := NewMockStorageSwapper(t)
mockWorker := jobs.NewMockWorker(t)
// Create a wrapper function that calls the provided function
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
if clone.BeforeFn != nil {
if err := clone.BeforeFn(); err != nil {
return err
}
}
if push.BeforeFn != nil {
if err := push.BeforeFn(); err != nil {
return err
}
}
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return errors.New("abort test here")
}
@@ -376,8 +363,7 @@ func TestLegacyMigrator_BeforeFnExecution(t *testing.T) {
)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "clone repository").Return()
progress.On("SetMessage", mock.Anything, "push changes").Return()
// No progress messages expected in current staging implementation
// Execute
repo := repository.NewMockRepository(t)
@@ -399,19 +385,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) {
mockWorker := jobs.NewMockWorker(t)
// Create a wrapper function that calls the provided function
wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error {
if clone.Progress != nil {
if _, err := clone.Progress.Write([]byte("clone repository\n")); err != nil {
return fmt.Errorf("failed to write to clone progress in tests: %w", err)
}
}
if push.Progress != nil {
if _, err := push.Progress.Write([]byte("push changes\n")); err != nil {
return fmt.Errorf("failed to write to push progress in tests: %w", err)
}
}
wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error {
return errors.New("abort test here")
}
@@ -423,8 +397,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) {
)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "clone repository").Return()
progress.On("SetMessage", mock.Anything, "push changes").Return()
// No progress messages expected in current staging implementation
repo := repository.NewMockRepository(t)
repo.On("Config").Return(&provisioning.Repository{
@@ -437,10 +410,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) {
require.EqualError(t, err, "migrate from SQL: abort test here")
require.Eventually(t, func() bool {
if len(progress.Calls) != 2 {
return false
}
// No progress message calls expected in current staging implementation
return progress.AssertExpectations(t)
}, time.Second, 10*time.Millisecond)
})

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package migrate

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package migrate

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package migrate

View File

@@ -1,87 +0,0 @@
// Code generated by mockery v2.52.4. DO NOT EDIT.
package migrate
import (
context "context"
repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
mock "github.com/stretchr/testify/mock"
)
// MockWrapWithCloneFn is an autogenerated mock type for the WrapWithCloneFn type
type MockWrapWithCloneFn struct {
mock.Mock
}
type MockWrapWithCloneFn_Expecter struct {
mock *mock.Mock
}
func (_m *MockWrapWithCloneFn) EXPECT() *MockWrapWithCloneFn_Expecter {
return &MockWrapWithCloneFn_Expecter{mock: &_m.Mock}
}
// Execute provides a mock function with given fields: ctx, repo, cloneOptions, pushOptions, fn
func (_m *MockWrapWithCloneFn) Execute(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error) error {
ret := _m.Called(ctx, repo, cloneOptions, pushOptions, fn)
if len(ret) == 0 {
panic("no return value specified for Execute")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error); ok {
r0 = rf(ctx, repo, cloneOptions, pushOptions, fn)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockWrapWithCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute'
type MockWrapWithCloneFn_Execute_Call struct {
*mock.Call
}
// Execute is a helper method to define mock.On call
// - ctx context.Context
// - repo repository.Repository
// - cloneOptions repository.CloneOptions
// - pushOptions repository.PushOptions
// - fn func(repository.Repository , bool) error
func (_e *MockWrapWithCloneFn_Expecter) Execute(ctx interface{}, repo interface{}, cloneOptions interface{}, pushOptions interface{}, fn interface{}) *MockWrapWithCloneFn_Execute_Call {
return &MockWrapWithCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, cloneOptions, pushOptions, fn)}
}
func (_c *MockWrapWithCloneFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error)) *MockWrapWithCloneFn_Execute_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.CloneOptions), args[3].(repository.PushOptions), args[4].(func(repository.Repository, bool) error))
})
return _c
}
func (_c *MockWrapWithCloneFn_Execute_Call) Return(_a0 error) *MockWrapWithCloneFn_Execute_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWrapWithCloneFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error) *MockWrapWithCloneFn_Execute_Call {
_c.Call.Return(run)
return _c
}
// NewMockWrapWithCloneFn creates a new instance of MockWrapWithCloneFn. 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 NewMockWrapWithCloneFn(t interface {
mock.TestingT
Cleanup(func())
}) *MockWrapWithCloneFn {
mock := &MockWrapWithCloneFn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -9,8 +9,8 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
)
//go:generate mockery --name WrapWithCloneFn --structname MockWrapWithCloneFn --inpackage --filename mock_wrap_with_clone_fn.go --with-expecter
type WrapWithCloneFn func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repo repository.Repository, cloned bool) error) error
//go:generate mockery --name WrapWithStageFn --structname MockWrapWithStageFn --inpackage --filename mock_wrap_with_stage_fn.go --with-expecter
type WrapWithStageFn func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repo repository.Repository, staged bool) error) error
type UnifiedStorageMigrator struct {
namespaceCleaner NamespaceCleaner

View File

@@ -12,6 +12,7 @@ import (
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"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/local"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
)
@@ -88,7 +89,7 @@ func TestMigrationWorker_WithHistory(t *testing.T) {
progressRecorder.On("SetTotal", mock.Anything, 10).Return()
progressRecorder.On("Strict").Return()
repo := repository.NewLocal(&provisioning.Repository{}, nil)
repo := local.NewLocal(&provisioning.Repository{}, nil)
err := worker.Process(context.Background(), repo, job, progressRecorder)
require.EqualError(t, err, "history is only supported for github repositories")
})

View File

@@ -3,7 +3,6 @@ package sync
import (
"context"
"fmt"
"sort"
"strings"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
@@ -143,17 +142,7 @@ func Changes(source []repository.FileTreeEntry, target *provisioning.ResourceLis
}
// Deepest first (stable sort order)
sort.Slice(changes, func(i, j int) bool {
if safepath.Depth(changes[i].Path) > safepath.Depth(changes[j].Path) {
return true
}
if safepath.Depth(changes[i].Path) < safepath.Depth(changes[j].Path) {
return false
}
return changes[i].Path < changes[j].Path
})
safepath.SortByDepth(changes, func(c ResourceFileChange) string { return c.Path }, false)
return changes, nil
}

View File

@@ -45,9 +45,9 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/migrate"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/sync"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/nanogit"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/local"
"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"
@@ -76,7 +76,7 @@ type APIBuilder struct {
features featuremgmt.FeatureToggles
getter rest.Getter
localFileResolver *repository.LocalFolderResolver
localFileResolver *local.LocalFolderResolver
parsers resources.ParserFactory
repositoryResources resources.RepositoryResourcesFactory
clients resources.ClientFactory
@@ -105,7 +105,7 @@ type APIBuilder struct {
// It avoids anything that is core to Grafana, such that it can be used in a multi-tenant service down the line.
// This means there are no hidden dependencies, and no use of e.g. *settings.Cfg.
func NewAPIBuilder(
local *repository.LocalFolderResolver,
local *local.LocalFolderResolver,
features featuremgmt.FeatureToggles,
unified resource.ResourceClient,
clonedir string, // where repo clones are managed
@@ -168,14 +168,7 @@ 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{
folderResolver := &local.LocalFolderResolver{
PermittedPrefixes: cfg.PermittedProvisioningPaths,
HomePath: safepath.Clean(cfg.HomePath),
}
@@ -606,11 +599,12 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
b.repositoryLister = repoInformer.Lister()
stageIfPossible := repository.WrapWithStageAndPushIfPossible
exportWorker := export.NewExportWorker(
b.clients,
b.repositoryResources,
export.ExportAll,
repository.WrapWithCloneAndPushIfPossible,
stageIfPossible,
)
b.statusPatcher = controller.NewRepositoryStatusPatcher(b.GetClient())
@@ -636,7 +630,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
legacyResources,
storageSwapper,
syncWorker,
repository.WrapWithCloneAndPushIfPossible,
stageIfPossible,
)
cleaner := migrate.NewNamespaceCleaner(b.clients)
@@ -1170,52 +1164,63 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
switch r.Spec.Type {
case provisioning.LocalRepositoryType:
return repository.NewLocal(r, b.localFileResolver), nil
return local.NewLocal(r, b.localFileResolver), nil
case provisioning.GitRepositoryType:
return nanogit.NewGitRepository(ctx, b.secrets, r, nanogit.RepositoryConfig{
// Decrypt token if needed
token := r.Spec.Git.Token
if token == "" {
decrypted, err := b.secrets.Decrypt(ctx, r.Spec.Git.EncryptedToken)
if err != nil {
return nil, fmt.Errorf("decrypt git token: %w", err)
}
token = string(decrypted)
}
return git.NewGitRepository(ctx, r, git.RepositoryConfig{
URL: r.Spec.Git.URL,
Branch: r.Spec.Git.Branch,
Path: r.Spec.Git.Path,
Token: r.Spec.Git.Token,
Token: token,
EncryptedToken: r.Spec.Git.EncryptedToken,
})
case provisioning.GitHubRepositoryType:
cloneFn := func(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) {
return gogit.Clone(ctx, b.clonedir, r, opts, b.secrets)
}
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")
logger.Info("Instantiating Github repository")
ghCfg := r.Spec.GitHub
if ghCfg == nil {
return nil, fmt.Errorf("github configuration is required for nano git")
}
gitCfg := nanogit.RepositoryConfig{
// Decrypt GitHub token if needed
ghToken := ghCfg.Token
if ghToken == "" && len(ghCfg.EncryptedToken) > 0 {
decrypted, err := b.secrets.Decrypt(ctx, ghCfg.EncryptedToken)
if err != nil {
return nil, fmt.Errorf("decrypt github token: %w", err)
}
ghToken = string(decrypted)
}
gitCfg := git.RepositoryConfig{
URL: ghCfg.URL,
Branch: ghCfg.Branch,
Path: ghCfg.Path,
Token: ghCfg.Token,
Token: ghToken,
EncryptedToken: ghCfg.EncryptedToken,
}
nanogitRepo, err := nanogit.NewGitRepository(ctx, b.secrets, r, gitCfg)
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg)
if err != nil {
return nil, fmt.Errorf("error creating nanogit repository: %w", err)
return nil, fmt.Errorf("error creating git repository: %w", err)
}
return nanogit.NewGithubRepository(apiRepo, nanogitRepo), nil
ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, ghToken)
if err != nil {
return nil, fmt.Errorf("error creating github repository: %w", err)
}
return ghRepo, nil
default:
return nil, fmt.Errorf("unknown repository type (%s)", r.Spec.Type)
}

View File

@@ -1,95 +0,0 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
package repository
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// MockClonableRepository is an autogenerated mock type for the ClonableRepository type
type MockClonableRepository struct {
mock.Mock
}
type MockClonableRepository_Expecter struct {
mock *mock.Mock
}
func (_m *MockClonableRepository) EXPECT() *MockClonableRepository_Expecter {
return &MockClonableRepository_Expecter{mock: &_m.Mock}
}
// Clone provides a mock function with given fields: ctx, opts
func (_m *MockClonableRepository) 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
}
// MockClonableRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone'
type MockClonableRepository_Clone_Call struct {
*mock.Call
}
// Clone is a helper method to define mock.On call
// - ctx context.Context
// - opts CloneOptions
func (_e *MockClonableRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockClonableRepository_Clone_Call {
return &MockClonableRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)}
}
func (_c *MockClonableRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockClonableRepository_Clone_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(CloneOptions))
})
return _c
}
func (_c *MockClonableRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockClonableRepository_Clone_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClonableRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockClonableRepository_Clone_Call {
_c.Call.Return(run)
return _c
}
// NewMockClonableRepository creates a new instance of MockClonableRepository. 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 NewMockClonableRepository(t interface {
mock.TestingT
Cleanup(func())
}) *MockClonableRepository {
mock := &MockClonableRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,44 +0,0 @@
package repository
import (
context "context"
"fmt"
"github.com/grafana/grafana-app-sdk/logging"
)
// WrapWithCloneAndPushIfPossible clones a repository if possible, executes operations on the clone,
// and automatically pushes changes when the function completes. For repositories that support cloning,
// all operations are transparently executed on the clone, and the clone is automatically cleaned up
// afterward. If cloning is not supported, the original repository instance is used directly.
func WrapWithCloneAndPushIfPossible(
ctx context.Context,
repo Repository,
cloneOptions CloneOptions,
pushOptions PushOptions,
fn func(repo Repository, cloned bool) error,
) error {
clonable, ok := repo.(ClonableRepository)
if !ok {
return fn(repo, false)
}
clone, err := clonable.Clone(ctx, cloneOptions)
if err != nil {
return fmt.Errorf("clone repository: %w", err)
}
// We don't, we simply log it
// FIXME: should we handle this differently?
defer func() {
if err := clone.Remove(ctx); err != nil {
logging.FromContext(ctx).Error("failed to remove cloned repository after export", "err", err)
}
}()
if err := fn(clone, true); err != nil {
return err
}
return clone.Push(ctx, pushOptions)
}

View File

@@ -1,95 +0,0 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
package repository
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// MockCloneFn is an autogenerated mock type for the CloneFn type
type MockCloneFn struct {
mock.Mock
}
type MockCloneFn_Expecter struct {
mock *mock.Mock
}
func (_m *MockCloneFn) EXPECT() *MockCloneFn_Expecter {
return &MockCloneFn_Expecter{mock: &_m.Mock}
}
// Execute provides a mock function with given fields: ctx, opts
func (_m *MockCloneFn) Execute(ctx context.Context, opts CloneOptions) (ClonedRepository, error) {
ret := _m.Called(ctx, opts)
if len(ret) == 0 {
panic("no return value specified for Execute")
}
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
}
// MockCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute'
type MockCloneFn_Execute_Call struct {
*mock.Call
}
// Execute is a helper method to define mock.On call
// - ctx context.Context
// - opts CloneOptions
func (_e *MockCloneFn_Expecter) Execute(ctx interface{}, opts interface{}) *MockCloneFn_Execute_Call {
return &MockCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, opts)}
}
func (_c *MockCloneFn_Execute_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockCloneFn_Execute_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(CloneOptions))
})
return _c
}
func (_c *MockCloneFn_Execute_Call) Return(_a0 ClonedRepository, _a1 error) *MockCloneFn_Execute_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCloneFn_Execute_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockCloneFn_Execute_Call {
_c.Call.Return(run)
return _c
}
// NewMockCloneFn creates a new instance of MockCloneFn. 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 NewMockCloneFn(t interface {
mock.TestingT
Cleanup(func())
}) *MockCloneFn {
mock := &MockCloneFn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,144 +0,0 @@
package repository
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockClonableRepo struct {
*MockClonableRepository
*MockClonedRepository
}
func Test_WrapWithCloneAndPushIfPossible_NonClonableRepository(t *testing.T) {
nonClonable := NewMockRepository(t)
var called bool
fn := func(repo Repository, cloned bool) error {
called = true
return errors.New("operation failed")
}
err := WrapWithCloneAndPushIfPossible(context.Background(), nonClonable, CloneOptions{}, PushOptions{}, fn)
require.EqualError(t, err, "operation failed")
require.True(t, called)
}
func TestWrapWithCloneAndPushIfPossible(t *testing.T) {
tests := []struct {
name string
setupMocks func(t *testing.T) *mockClonableRepo
operation func(repo Repository, cloned bool) error
expectedError string
}{
{
name: "successful clone, operation, and push",
setupMocks: func(t *testing.T) *mockClonableRepo {
mockRepo := NewMockClonableRepository(t)
mockCloned := NewMockClonedRepository(t)
mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil)
mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(nil)
mockCloned.EXPECT().Remove(mock.Anything).Return(nil)
return &mockClonableRepo{
MockClonableRepository: mockRepo,
MockClonedRepository: mockCloned,
}
},
operation: func(repo Repository, cloned bool) error {
require.True(t, cloned)
return nil
},
},
{
name: "clone failure",
setupMocks: func(t *testing.T) *mockClonableRepo {
mockRepo := NewMockClonableRepository(t)
mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(nil, errors.New("clone failed"))
return &mockClonableRepo{
MockClonableRepository: mockRepo,
}
},
operation: func(repo Repository, cloned bool) error {
return nil
},
expectedError: "clone repository: clone failed",
},
{
name: "operation failure",
setupMocks: func(t *testing.T) *mockClonableRepo {
mockRepo := NewMockClonableRepository(t)
mockCloned := NewMockClonedRepository(t)
mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil)
mockCloned.EXPECT().Remove(mock.Anything).Return(nil)
return &mockClonableRepo{
MockClonableRepository: mockRepo,
MockClonedRepository: mockCloned,
}
},
operation: func(repo Repository, cloned bool) error {
return errors.New("operation failed")
},
expectedError: "operation failed",
},
{
name: "push failure",
setupMocks: func(t *testing.T) *mockClonableRepo {
mockRepo := NewMockClonableRepository(t)
mockCloned := NewMockClonedRepository(t)
mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil)
mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(errors.New("push failed"))
mockCloned.EXPECT().Remove(mock.Anything).Return(nil)
return &mockClonableRepo{
MockClonableRepository: mockRepo,
MockClonedRepository: mockCloned,
}
},
operation: func(repo Repository, cloned bool) error {
return nil
},
expectedError: "push failed",
},
{
name: "remove failure should only log",
setupMocks: func(t *testing.T) *mockClonableRepo {
mockRepo := NewMockClonableRepository(t)
mockCloned := NewMockClonedRepository(t)
mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil)
mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(nil)
mockCloned.EXPECT().Remove(mock.Anything).Return(errors.New("remove failed"))
return &mockClonableRepo{
MockClonableRepository: mockRepo,
MockClonedRepository: mockCloned,
}
},
operation: func(repo Repository, cloned bool) error {
return nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := tt.setupMocks(t)
err := WrapWithCloneAndPushIfPossible(context.Background(), repo, CloneOptions{}, PushOptions{}, tt.operation)
if tt.expectedError != "" {
require.EqualError(t, err, tt.expectedError)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository

View File

@@ -0,0 +1,33 @@
package git
import (
"regexp"
"strings"
)
// basicGitBranchNameRegex is a regular expression to validate a git branch name
// 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.
// 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.
// 3. The branch name cannot contain consecutive slashes (`//`).
// 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 {
if !basicGitBranchNameRegex.MatchString(branch) {
return false
}
// Additional checks for invalid patterns
if strings.HasPrefix(branch, "/") || strings.HasSuffix(branch, "/") ||
strings.HasSuffix(branch, ".") || strings.Contains(branch, "..") ||
strings.Contains(branch, "//") || strings.HasSuffix(branch, ".lock") {
return false
}
return true
}

View File

@@ -0,0 +1,47 @@
package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsValidGitBranchName(t *testing.T) {
tests := []struct {
name string
branch string
expected bool
}{
{"Valid branch name", "feature/add-tests", true},
{"Valid branch name with numbers", "feature/123-add-tests", true},
{"Valid branch name with dots", "feature.add.tests", true},
{"Valid branch name with hyphens", "feature-add-tests", true},
{"Valid branch name with underscores", "feature_add_tests", true},
{"Valid branch name with mixed characters", "feature/add_tests-123", true},
{"Starts with /", "/feature", false},
{"Ends with /", "feature/", false},
{"Ends with .", "feature.", false},
{"Ends with space", "feature ", false},
{"Contains consecutive slashes", "feature//branch", false},
{"Contains consecutive dots", "feature..branch", false},
{"Contains @{", "feature@{branch", false},
{"Contains invalid character ~", "feature~branch", false},
{"Contains invalid character ^", "feature^branch", false},
{"Contains invalid character :", "feature:branch", false},
{"Contains invalid character ?", "feature?branch", false},
{"Contains invalid character *", "feature*branch", false},
{"Contains invalid character [", "feature[branch", false},
{"Contains invalid character ]", "feature]branch", false},
{"Contains invalid character \\", "feature\\branch", false},
{"Empty branch name", "", false},
{"Only whitespace", " ", false},
{"Single valid character", "a", true},
{"Ends with .lock", "feature.lock", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, IsValidGitBranchName(tt.branch))
})
}
}

View File

@@ -1,15 +1,17 @@
package repository
package git
import "github.com/grafana/grafana/pkg/registry/apis/provisioning/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
repository.Repository
repository.Versioned
repository.Writer
repository.Reader
repository.StageableRepository
URL() string
Branch() string
}

View File

@@ -1,6 +1,6 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository
package git
import (
context "context"
@@ -8,6 +8,8 @@ import (
mock "github.com/stretchr/testify/mock"
field "k8s.io/apimachinery/pkg/util/validation/field"
repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
@@ -69,83 +71,24 @@ func (_c *MockGitRepository_Branch_Call) RunAndReturn(run func() string) *MockGi
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) {
func (_m *MockGitRepository) CompareFiles(ctx context.Context, base string, ref string) ([]repository.VersionedFileChange, error) {
ret := _m.Called(ctx, base, ref)
if len(ret) == 0 {
panic("no return value specified for CompareFiles")
}
var r0 []VersionedFileChange
var r0 []repository.VersionedFileChange
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]VersionedFileChange, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]repository.VersionedFileChange, error)); ok {
return rf(ctx, base, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) []VersionedFileChange); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string) []repository.VersionedFileChange); ok {
r0 = rf(ctx, base, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]VersionedFileChange)
r0 = ret.Get(0).([]repository.VersionedFileChange)
}
}
@@ -178,12 +121,12 @@ func (_c *MockGitRepository_CompareFiles_Call) Run(run func(ctx context.Context,
return _c
}
func (_c *MockGitRepository_CompareFiles_Call) Return(_a0 []VersionedFileChange, _a1 error) *MockGitRepository_CompareFiles_Call {
func (_c *MockGitRepository_CompareFiles_Call) Return(_a0 []repository.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 {
func (_c *MockGitRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]repository.VersionedFileChange, error)) *MockGitRepository_CompareFiles_Call {
_c.Call.Return(run)
return _c
}
@@ -451,23 +394,23 @@ func (_c *MockGitRepository_LatestRef_Call) RunAndReturn(run func(context.Contex
}
// Read provides a mock function with given fields: ctx, path, ref
func (_m *MockGitRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) {
func (_m *MockGitRepository) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) {
ret := _m.Called(ctx, path, ref)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 *FileInfo
var r0 *repository.FileInfo
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string) (*repository.FileInfo, error)); ok {
return rf(ctx, path, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string) *repository.FileInfo); ok {
r0 = rf(ctx, path, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*FileInfo)
r0 = ret.Get(0).(*repository.FileInfo)
}
}
@@ -500,34 +443,34 @@ func (_c *MockGitRepository_Read_Call) Run(run func(ctx context.Context, path st
return _c
}
func (_c *MockGitRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockGitRepository_Read_Call {
func (_c *MockGitRepository_Read_Call) Return(_a0 *repository.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 {
func (_c *MockGitRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*repository.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) {
func (_m *MockGitRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) {
ret := _m.Called(ctx, ref)
if len(ret) == 0 {
panic("no return value specified for ReadTree")
}
var r0 []FileTreeEntry
var r0 []repository.FileTreeEntry
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, string) ([]repository.FileTreeEntry, error)); ok {
return rf(ctx, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok {
if rf, ok := ret.Get(0).(func(context.Context, string) []repository.FileTreeEntry); ok {
r0 = rf(ctx, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]FileTreeEntry)
r0 = ret.Get(0).([]repository.FileTreeEntry)
}
}
@@ -559,12 +502,71 @@ func (_c *MockGitRepository_ReadTree_Call) Run(run func(ctx context.Context, ref
return _c
}
func (_c *MockGitRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockGitRepository_ReadTree_Call {
func (_c *MockGitRepository_ReadTree_Call) Return(_a0 []repository.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 {
func (_c *MockGitRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]repository.FileTreeEntry, error)) *MockGitRepository_ReadTree_Call {
_c.Call.Return(run)
return _c
}
// Stage provides a mock function with given fields: ctx, opts
func (_m *MockGitRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) {
ret := _m.Called(ctx, opts)
if len(ret) == 0 {
panic("no return value specified for Stage")
}
var r0 repository.StagedRepository
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) (repository.StagedRepository, error)); ok {
return rf(ctx, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) repository.StagedRepository); ok {
r0 = rf(ctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(repository.StagedRepository)
}
}
if rf, ok := ret.Get(1).(func(context.Context, repository.StageOptions) error); ok {
r1 = rf(ctx, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage'
type MockGitRepository_Stage_Call struct {
*mock.Call
}
// Stage is a helper method to define mock.On call
// - ctx context.Context
// - opts repository.StageOptions
func (_e *MockGitRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockGitRepository_Stage_Call {
return &MockGitRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)}
}
func (_c *MockGitRepository_Stage_Call) Run(run func(ctx context.Context, opts repository.StageOptions)) *MockGitRepository_Stage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(repository.StageOptions))
})
return _c
}
func (_c *MockGitRepository_Stage_Call) Return(_a0 repository.StagedRepository, _a1 error) *MockGitRepository_Stage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_Stage_Call) RunAndReturn(run func(context.Context, repository.StageOptions) (repository.StagedRepository, error)) *MockGitRepository_Stage_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -1,12 +1,14 @@
package nanogit
package git
import (
"bytes"
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -17,7 +19,6 @@ import (
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"
@@ -38,28 +39,19 @@ 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)
) (GitRepository, error) {
var opts []options.Option
if len(gitConfig.Token) > 0 {
opts = append(opts, options.WithBasicAuth("git", gitConfig.Token))
}
// Create nanogit client with authentication
client, err := nanogit.NewHTTPClient(
gitConfig.URL,
options.WithBasicAuth("git", gitConfig.Token),
)
client, err := nanogit.NewHTTPClient(gitConfig.URL, opts...)
if err != nil {
return nil, fmt.Errorf("create nanogit client: %w", err)
}
@@ -68,7 +60,6 @@ func NewGitRepository(
config: config,
gitConfig: gitConfig,
client: client,
secrets: secrets,
}, nil
}
@@ -98,12 +89,15 @@ func (r *gitRepository) Validate() (list field.ErrorList) {
}
if cfg.Branch == "" {
list = append(list, field.Required(field.NewPath("spec", t, "branch"), "a git branch is required"))
} else if !repository.IsValidGitBranchName(cfg.Branch) {
} else if !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 the repository has workflows, we require a token or encrypted token
if len(r.config.Spec.Workflows) > 0 {
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 {
@@ -412,11 +406,15 @@ func (r *gitRepository) Write(ctx context.Context, path string, ref string, data
}
ctx, _ = r.logger(ctx, ref)
_, err := r.Read(ctx, path, ref)
info, 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 {
// If the value already exists and is the same, we don't need to do anything
if bytes.Equal(info.Data, data) {
return nil
}
return r.Update(ctx, path, ref, data, message)
}
@@ -450,7 +448,8 @@ func (r *gitRepository) delete(ctx context.Context, path string, writer nanogit.
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 {
trimmed := strings.TrimSuffix(finalPath, "/")
if _, err := writer.DeleteTree(ctx, trimmed); err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {
return repository.ErrFileNotFound
}
@@ -582,7 +581,7 @@ func (r *gitRepository) CompareFiles(ctx context.Context, base, ref string) ([]r
return changes, nil
}
func (r *gitRepository) Clone(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) {
func (r *gitRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) {
return NewStagedGitRepository(ctx, r, opts)
}
@@ -590,7 +589,7 @@ func (r *gitRepository) Clone(ctx context.Context, opts repository.CloneOptions)
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)
ref = r.gitConfig.Branch
}
// Try to parse ref as a hash first
@@ -600,6 +599,9 @@ func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash.
return refHash, nil
}
// Prefix ref with refs/heads/
ref = fmt.Sprintf("refs/heads/%s", ref)
// Not a valid hash, try to resolve as a branch reference
branchRef, err := r.client.GetRef(ctx, ref)
if err != nil {
@@ -615,7 +617,7 @@ func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash.
// 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) {
if !IsValidGitBranchName(branchName) {
return nanogit.Ref{}, &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusBadRequest,

View File

@@ -1,4 +1,4 @@
package nanogit
package git
import (
"context"
@@ -7,17 +7,17 @@ import (
"testing"
"time"
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/secrets"
"github.com/grafana/nanogit"
"github.com/grafana/nanogit/mocks"
"github.com/grafana/nanogit/protocol"
"github.com/grafana/nanogit/protocol/hash"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
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"
"github.com/grafana/nanogit/protocol/hash"
)
func TestGitRepository_Validate(t *testing.T) {
@@ -138,10 +138,11 @@ func TestGitRepository_Validate(t *testing.T) {
},
},
{
name: "missing token",
name: "missing token for R/W repository",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: "test_type",
Type: "test_type",
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow},
},
},
gitConfig: RepositoryConfig{
@@ -153,6 +154,21 @@ func TestGitRepository_Validate(t *testing.T) {
field.Required(field.NewPath("spec", "test_type", "token"), "a git access token is required"),
},
},
{
name: "missing token for read-only repository",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: "test_type",
Workflows: nil, // read-only
},
},
gitConfig: RepositoryConfig{
URL: "https://git.example.com/repo.git",
Branch: "main",
Token: "", // Empty token
},
want: nil,
},
{
name: "unsafe path",
config: &provisioning.Repository{
@@ -258,20 +274,8 @@ func TestIsValidGitURL(t *testing.T) {
}
}
// Mock secrets service for testing
type mockSecretsService struct{}
func (m *mockSecretsService) Decrypt(ctx context.Context, data []byte) ([]byte, error) {
return []byte("decrypted-token"), nil
}
func (m *mockSecretsService) Encrypt(ctx context.Context, data []byte) ([]byte, error) {
return []byte("encrypted-token"), nil
}
func TestNewGit(t *testing.T) {
ctx := context.Background()
mockSecrets := &mockSecretsService{}
config := &provisioning.Repository{
Spec: provisioning.RepositorySpec{
@@ -288,7 +292,7 @@ func TestNewGit(t *testing.T) {
// This should succeed in creating the client but won't be able to connect
// We just test that the basic structure is created correctly
gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig)
gitRepo, err := NewGitRepository(ctx, config, gitConfig)
require.NoError(t, err)
require.NotNil(t, gitRepo)
require.Equal(t, "https://git.example.com/owner/repo.git", gitRepo.URL())
@@ -1073,27 +1077,27 @@ func TestGitRepository_Update(t *testing.T) {
func TestGitRepository_Delete(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeClient)
gitConfig RepositoryConfig
path string
ref string
comment string
wantError bool
errorType error
name string
setupMock func(*mocks.FakeClient, *mocks.FakeStagedWriter)
assertions func(*testing.T, *mocks.FakeClient, *mocks.FakeStagedWriter)
gitConfig RepositoryConfig
path string
ref string
comment string
wantError bool
errorType error
}{
{
name: "success - delete file",
setupMock: func(mockClient *mocks.FakeClient) {
setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{},
}, nil)
mockWriter := &mocks.FakeStagedWriter{}
mockWriter.DeleteBlobReturns(hash.Hash{}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
gitConfig: RepositoryConfig{
Branch: "main",
@@ -1106,16 +1110,19 @@ func TestGitRepository_Delete(t *testing.T) {
},
{
name: "success - delete directory",
setupMock: func(mockClient *mocks.FakeClient) {
setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{},
}, nil)
mockWriter := &mocks.FakeStagedWriter{}
mockWriter.DeleteTreeReturns(hash.Hash{}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
assertions: func(t *testing.T, fakeClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) {
require.Equal(t, 1, mockWriter.DeleteTreeCallCount(), "DeleteTree should be called once")
_, p := mockWriter.DeleteTreeArgsForCall(0)
require.Equal(t, "configs/testdir", p, "DeleteTree should be called with correct path")
},
gitConfig: RepositoryConfig{
Branch: "main",
@@ -1128,14 +1135,12 @@ func TestGitRepository_Delete(t *testing.T) {
},
{
name: "failure - file not found",
setupMock: func(mockClient *mocks.FakeClient) {
setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{},
}, nil)
mockWriter := &mocks.FakeStagedWriter{}
mockWriter.DeleteBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound)
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
gitConfig: RepositoryConfig{
Branch: "main",
@@ -1152,7 +1157,10 @@ func TestGitRepository_Delete(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := &mocks.FakeClient{}
tt.setupMock(mockClient)
mockWriter := &mocks.FakeStagedWriter{}
mockClient.NewStagedWriterReturns(mockWriter, nil)
tt.setupMock(mockClient, mockWriter)
gitRepo := &gitRepository{
client: mockClient,
@@ -1174,6 +1182,10 @@ func TestGitRepository_Delete(t *testing.T) {
} else {
require.NoError(t, err)
}
if tt.assertions != nil {
tt.assertions(t, mockClient, mockWriter)
}
})
}
}
@@ -1729,57 +1741,21 @@ func TestGitRepository_createSignature(t *testing.T) {
func TestNewGitRepository(t *testing.T) {
tests := []struct {
name string
setupMock func(*mockSecretsService)
gitConfig RepositoryConfig
wantError bool
expectURL string
expectToken string
name string
gitConfig RepositoryConfig
wantError bool
expectURL string
}{
{
name: "success - with token",
setupMock: func(mockSecrets *mockSecretsService) {
// No setup needed for token case
},
gitConfig: RepositoryConfig{
URL: "https://git.example.com/owner/repo.git",
Branch: "main",
Token: "plain-token",
Path: "configs",
},
wantError: false,
expectURL: "https://git.example.com/owner/repo.git",
expectToken: "plain-token",
},
{
name: "success - with encrypted token",
setupMock: func(mockSecrets *mockSecretsService) {
// Mock will return decrypted token
},
gitConfig: RepositoryConfig{
URL: "https://git.example.com/owner/repo.git",
Branch: "main",
Token: "", // Empty token, will use encrypted
EncryptedToken: []byte("encrypted-token"),
Path: "configs",
},
wantError: false,
expectURL: "https://git.example.com/owner/repo.git",
expectToken: "decrypted-token", // From mock
},
{
name: "failure - decryption error",
setupMock: func(mockSecrets *mockSecretsService) {
// This test will use the separate mockSecretsServiceWithError
},
gitConfig: RepositoryConfig{
URL: "https://git.example.com/owner/repo.git",
Branch: "main",
Token: "",
EncryptedToken: []byte("bad-encrypted-token"),
Path: "configs",
},
wantError: true,
wantError: false,
expectURL: "https://git.example.com/owner/repo.git",
},
}
@@ -1787,20 +1763,13 @@ func TestNewGitRepository(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
var mockSecrets secrets.Service
if tt.name == "failure - decryption error" {
mockSecrets = &mockSecretsServiceWithError{shouldError: true}
} else {
mockSecrets = &mockSecretsService{}
}
config := &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
},
}
gitRepo, err := NewGitRepository(ctx, mockSecrets, config, tt.gitConfig)
gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig)
if tt.wantError {
require.Error(t, err)
@@ -2168,7 +2137,7 @@ func TestGitRepository_logger(t *testing.T) {
})
}
func TestGitRepository_Clone(t *testing.T) {
func TestGitRepository_Stage(t *testing.T) {
gitRepo := &gitRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
@@ -2184,9 +2153,8 @@ func TestGitRepository_Clone(t *testing.T) {
t.Run("calls NewStagedGitRepository", func(t *testing.T) {
ctx := context.Background()
opts := repository.CloneOptions{
CreateIfNotExists: true,
PushOnWrites: true,
opts := repository.StageOptions{
PushOnWrites: true,
}
// Since NewStagedGitRepository is not mocked and may panic, we expect this to fail
@@ -2198,11 +2166,11 @@ func TestGitRepository_Clone(t *testing.T) {
}
}()
cloned, err := gitRepo.Clone(ctx, opts)
staged, err := gitRepo.Stage(ctx, opts)
// This will likely error/panic since we don't have a real implementation
// but we're testing that the method exists and forwards to NewStagedGitRepository
_ = cloned
_ = staged
_ = err
})
}
@@ -2271,50 +2239,6 @@ func TestGitRepository_EdgeCases(t *testing.T) {
})
}
// Enhanced secrets service mock with error handling
type mockSecretsServiceWithError struct {
shouldError bool
}
func (m *mockSecretsServiceWithError) Decrypt(ctx context.Context, data []byte) ([]byte, error) {
if m.shouldError {
return nil, errors.New("decryption failed")
}
return []byte("decrypted-token"), nil
}
func (m *mockSecretsServiceWithError) Encrypt(ctx context.Context, data []byte) ([]byte, error) {
if m.shouldError {
return nil, errors.New("encryption failed")
}
return []byte("encrypted-token"), nil
}
func TestNewGitRepository_DecryptionError(t *testing.T) {
ctx := context.Background()
mockSecrets := &mockSecretsServiceWithError{shouldError: true}
config := &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
},
}
gitConfig := RepositoryConfig{
URL: "https://git.example.com/owner/repo.git",
Branch: "main",
Token: "",
EncryptedToken: []byte("encrypted-token"),
Path: "configs",
}
gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig)
require.Error(t, err)
require.Nil(t, gitRepo)
require.Contains(t, err.Error(), "decrypt token")
}
func TestGitRepository_ValidateBranchNames(t *testing.T) {
tests := []struct {
name string
@@ -2782,7 +2706,6 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) {
// This test would require mocking nanogit.NewHTTPClient which is difficult
// We test the path where the client creation would fail by using invalid URL
ctx := context.Background()
mockSecrets := &mockSecretsService{}
config := &provisioning.Repository{
Spec: provisioning.RepositorySpec{
@@ -2797,7 +2720,7 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) {
Path: "configs",
}
gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig)
gitRepo, err := NewGitRepository(ctx, config, gitConfig)
// We expect this to fail during client creation
require.Error(t, err)

View File

@@ -1,4 +1,4 @@
package nanogit
package git
import (
"context"
@@ -15,17 +15,11 @@ import (
// once that happens we could do more magic here.
type stagedGitRepository struct {
*gitRepository
opts repository.CloneOptions
opts repository.StageOptions
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
}
}
func NewStagedGitRepository(ctx context.Context, repo *gitRepository, opts repository.StageOptions) (repository.StagedRepository, error) {
if opts.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
@@ -89,28 +83,35 @@ func (r *stagedGitRepository) Create(ctx context.Context, path, ref string, data
}
if r.opts.PushOnWrites {
return r.Push(ctx, repository.PushOptions{})
return r.Push(ctx)
}
return nil
}
func (r *stagedGitRepository) blobExists(ctx context.Context, path string) (bool, error) {
if r.gitConfig.Path != "" {
path = safepath.Join(r.gitConfig.Path, path)
}
return r.writer.BlobExists(ctx, path)
}
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)
exists, err := r.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 {
if exists {
if err := r.update(ctx, path, data, r.writer); err != nil {
return err
}
} else {
if err := r.update(ctx, path, data, r.writer); err != nil {
if err := r.create(ctx, path, data, r.writer); err != nil {
return err
}
}
@@ -120,7 +121,7 @@ func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data
}
if r.opts.PushOnWrites {
return r.Push(ctx, repository.PushOptions{})
return r.Push(ctx)
}
return nil
@@ -144,7 +145,7 @@ func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data
}
if r.opts.PushOnWrites {
return r.Push(ctx, repository.PushOptions{})
return r.Push(ctx)
}
return nil
@@ -164,22 +165,16 @@ func (r *stagedGitRepository) Delete(ctx context.Context, path, ref, message str
}
if r.opts.PushOnWrites {
return r.Push(ctx, repository.PushOptions{})
return r.Push(ctx)
}
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 {
func (r *stagedGitRepository) Push(ctx context.Context) error {
if r.opts.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
ctx, cancel = context.WithTimeout(ctx, r.opts.Timeout)
defer cancel()
}

View File

@@ -1,4 +1,4 @@
package nanogit
package git
import (
"context"
@@ -18,7 +18,7 @@ func TestNewStagedGitRepository(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeClient)
opts repository.CloneOptions
opts repository.StageOptions
wantError error
}{
{
@@ -31,9 +31,8 @@ func TestNewStagedGitRepository(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
opts: repository.StageOptions{
PushOnWrites: false,
},
wantError: nil,
},
@@ -47,12 +46,8 @@ func TestNewStagedGitRepository(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
BeforeFn: func() error {
return nil
},
opts: repository.StageOptions{
PushOnWrites: false,
},
wantError: nil,
},
@@ -66,33 +61,19 @@ func TestNewStagedGitRepository(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
Timeout: time.Second * 5,
opts: repository.StageOptions{
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,
opts: repository.StageOptions{
PushOnWrites: false,
},
wantError: errors.New("ref not found"),
},
@@ -105,9 +86,8 @@ func TestNewStagedGitRepository(t *testing.T) {
}, nil)
mockClient.NewStagedWriterReturns(nil, errors.New("failed to create writer"))
},
opts: repository.CloneOptions{
CreateIfNotExists: false,
PushOnWrites: false,
opts: repository.StageOptions{
PushOnWrites: false,
},
wantError: errors.New("build staged writer: failed to create writer"),
},
@@ -141,13 +121,8 @@ func TestNewStagedGitRepository(t *testing.T) {
// 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)
}
})
}
@@ -287,7 +262,7 @@ func TestStagedGitRepository_Create(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.CloneOptions
opts repository.StageOptions
path string
ref string
data []byte
@@ -301,7 +276,7 @@ func TestStagedGitRepository_Create(t *testing.T) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -318,7 +293,7 @@ func TestStagedGitRepository_Create(t *testing.T) {
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: true,
},
path: "test.yaml",
@@ -333,7 +308,7 @@ func TestStagedGitRepository_Create(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -347,7 +322,7 @@ func TestStagedGitRepository_Create(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create blob failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -362,7 +337,7 @@ func TestStagedGitRepository_Create(t *testing.T) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -378,7 +353,7 @@ func TestStagedGitRepository_Create(t *testing.T) {
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(errors.New("push failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: true,
},
path: "test.yaml",
@@ -419,7 +394,7 @@ func TestStagedGitRepository_Write(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.CloneOptions
opts repository.StageOptions
path string
ref string
data []byte
@@ -435,7 +410,7 @@ func TestStagedGitRepository_Write(t *testing.T) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -454,7 +429,7 @@ func TestStagedGitRepository_Write(t *testing.T) {
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: true,
},
path: "test.yaml",
@@ -470,7 +445,7 @@ func TestStagedGitRepository_Write(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -484,7 +459,7 @@ func TestStagedGitRepository_Write(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.BlobExistsReturns(false, errors.New("blob exists check failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -499,7 +474,7 @@ func TestStagedGitRepository_Write(t *testing.T) {
mockWriter.BlobExistsReturns(false, nil)
mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -514,7 +489,7 @@ func TestStagedGitRepository_Write(t *testing.T) {
mockWriter.BlobExistsReturns(true, nil)
mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -530,7 +505,7 @@ func TestStagedGitRepository_Write(t *testing.T) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -570,7 +545,7 @@ func TestStagedGitRepository_Update(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.CloneOptions
opts repository.StageOptions
path string
ref string
data []byte
@@ -584,7 +559,7 @@ func TestStagedGitRepository_Update(t *testing.T) {
mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -601,7 +576,7 @@ func TestStagedGitRepository_Update(t *testing.T) {
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: true,
},
path: "test.yaml",
@@ -616,7 +591,7 @@ func TestStagedGitRepository_Update(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -630,7 +605,7 @@ func TestStagedGitRepository_Update(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "directory/",
@@ -644,7 +619,7 @@ func TestStagedGitRepository_Update(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update blob failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -659,7 +634,7 @@ func TestStagedGitRepository_Update(t *testing.T) {
mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -699,7 +674,7 @@ func TestStagedGitRepository_Delete(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.CloneOptions
opts repository.StageOptions
path string
ref string
message string
@@ -712,7 +687,7 @@ func TestStagedGitRepository_Delete(t *testing.T) {
mockWriter.DeleteBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -728,7 +703,7 @@ func TestStagedGitRepository_Delete(t *testing.T) {
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: true,
},
path: "testdir/",
@@ -742,7 +717,7 @@ func TestStagedGitRepository_Delete(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
// No setup needed as error occurs before writer calls
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -755,7 +730,7 @@ func TestStagedGitRepository_Delete(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.DeleteBlobReturns(hash.Hash{}, errors.New("delete blob failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -769,7 +744,7 @@ func TestStagedGitRepository_Delete(t *testing.T) {
mockWriter.DeleteBlobReturns(hash.Hash{1, 2, 3}, nil)
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
opts: repository.CloneOptions{
opts: repository.StageOptions{
PushOnWrites: false,
},
path: "test.yaml",
@@ -808,7 +783,6 @@ func TestStagedGitRepository_Push(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
opts repository.PushOptions
wantError error
expectCalls int
}{
@@ -817,7 +791,6 @@ func TestStagedGitRepository_Push(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(nil)
},
opts: repository.PushOptions{},
wantError: nil,
expectCalls: 1,
},
@@ -826,11 +799,6 @@ func TestStagedGitRepository_Push(t *testing.T) {
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(nil)
},
opts: repository.PushOptions{
BeforeFn: func() error {
return nil
},
},
wantError: nil,
expectCalls: 1,
},
@@ -839,31 +807,14 @@ func TestStagedGitRepository_Push(t *testing.T) {
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,
},
@@ -874,9 +825,9 @@ func TestStagedGitRepository_Push(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
tt.setupMock(mockWriter)
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{})
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{})
err := stagedRepo.Push(context.Background(), tt.opts)
err := stagedRepo.Push(context.Background())
if tt.wantError != nil {
require.EqualError(t, err, tt.wantError.Error())
@@ -892,7 +843,7 @@ func TestStagedGitRepository_Push(t *testing.T) {
func TestStagedGitRepository_Remove(t *testing.T) {
t.Run("succeeds with remove", func(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{})
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{})
err := stagedRepo.Remove(context.Background())
require.NoError(t, err)
@@ -904,10 +855,10 @@ func TestStagedGitRepository_Remove(t *testing.T) {
func createTestStagedRepository(mockClient *mocks.FakeClient) *stagedGitRepository {
mockWriter := &mocks.FakeStagedWriter{}
return createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{}, mockClient)
return createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{}, mockClient)
}
func createTestStagedRepositoryWithWriter(mockWriter *mocks.FakeStagedWriter, opts repository.CloneOptions, mockClient ...*mocks.FakeClient) *stagedGitRepository {
func createTestStagedRepositoryWithWriter(mockWriter *mocks.FakeStagedWriter, opts repository.StageOptions, mockClient ...*mocks.FakeClient) *stagedGitRepository {
var client nanogit.Client
if len(mockClient) > 0 {
client = mockClient[0]

View File

@@ -1,683 +0,0 @@
package repository
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"regexp"
"strings"
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"
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
)
// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included.
type githubRepository struct {
config *provisioning.Repository
gh pgh.Client // assumes github.com base URL
secrets secrets.Service
owner string
repo string
cloneFn CloneFn
}
// 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
Writer
Reader
RepositoryWithURLs
ClonableRepository
Owner() string
Repo() string
Client() pgh.Client
}
func NewGitHub(
ctx context.Context,
config *provisioning.Repository,
factory *pgh.Factory,
secrets secrets.Service,
cloneFn CloneFn,
) (GithubRepository, error) {
owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL)
if err != nil {
return nil, fmt.Errorf("parse owner and repo: %w", err)
}
token := config.Spec.GitHub.Token
if token == "" {
decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken)
if err != nil {
return nil, fmt.Errorf("decrypt token: %w", err)
}
token = string(decrypted)
}
return &githubRepository{
config: config,
gh: factory.New(ctx, token), // TODO, baseURL from config
secrets: secrets,
owner: owner,
repo: repo,
cloneFn: cloneFn,
}, nil
}
func (r *githubRepository) Config() *provisioning.Repository {
return r.config
}
func (r *githubRepository) Owner() string {
return r.owner
}
func (r *githubRepository) Repo() string {
return r.repo
}
func (r *githubRepository) Client() pgh.Client {
return r.gh
}
// Validate implements provisioning.Repository.
func (r *githubRepository) Validate() (list field.ErrorList) {
gh := r.config.Spec.GitHub
if gh == nil {
list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required"))
return list
}
if gh.URL == "" {
list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required"))
} else {
_, _, err := ParseOwnerRepoGithub(gh.URL)
if err != nil {
list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error()))
} else if !strings.HasPrefix(gh.URL, "https://github.com/") {
list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/"))
}
}
if gh.Branch == "" {
list = append(list, field.Required(field.NewPath("spec", "github", "branch"), "a github branch is required"))
} 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
if gh.Token == "" && len(gh.EncryptedToken) == 0 {
list = append(list, field.Required(field.NewPath("spec", "github", "token"), "a github access token is required"))
}
if err := safepath.IsSafe(gh.Path); err != nil {
list = append(list, field.Invalid(field.NewPath("spec", "github", "prefix"), gh.Path, err.Error()))
}
if safepath.IsAbs(gh.Path) {
list = append(list, field.Invalid(field.NewPath("spec", "github", "prefix"), gh.Path, "path must be relative"))
}
return list
}
func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) {
parsed, e := url.Parse(strings.TrimSuffix(giturl, ".git"))
if e != nil {
err = e
return
}
parts := strings.Split(parsed.Path, "/")
if len(parts) < 3 {
err = fmt.Errorf("unable to parse repo+owner from url")
return
}
return parts[1], parts[2], nil
}
// Test implements provisioning.Repository.
func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) {
if err := r.gh.IsAuthenticated(ctx); err != nil {
return &provisioning.TestResults{
Code: http.StatusBadRequest,
Success: false,
Errors: []provisioning.ErrorDetails{{
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", "github", "token").String(),
Detail: err.Error(),
}}}, nil
}
url := r.config.Spec.GitHub.URL
owner, repo, err := ParseOwnerRepoGithub(url)
if err != nil {
return fromFieldError(field.Invalid(
field.NewPath("spec", "github", "url"), url, err.Error())), nil
}
// FIXME: check token permissions
ok, err := r.gh.RepoExists(ctx, owner, repo)
if err != nil {
return fromFieldError(field.Invalid(
field.NewPath("spec", "github", "url"), url, err.Error())), nil
}
if !ok {
return fromFieldError(field.NotFound(
field.NewPath("spec", "github", "url"), url)), nil
}
branch := r.config.Spec.GitHub.Branch
ok, err = r.gh.BranchExists(ctx, r.owner, r.repo, branch)
if err != nil {
return fromFieldError(field.Invalid(
field.NewPath("spec", "github", "branch"), branch, err.Error())), nil
}
if !ok {
return fromFieldError(field.NotFound(
field.NewPath("spec", "github", "branch"), branch)), nil
}
return &provisioning.TestResults{
Code: http.StatusOK,
Success: true,
}, nil
}
// ReadResource implements provisioning.Repository.
func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*FileInfo, error) {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
finalPath := safepath.Join(r.config.Spec.GitHub.Path, filePath)
content, dirContent, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref)
if err != nil {
if errors.Is(err, pgh.ErrResourceNotFound) {
return nil, ErrFileNotFound
}
return nil, fmt.Errorf("get contents: %w", err)
}
if dirContent != nil {
return &FileInfo{
Path: filePath,
Ref: ref,
}, nil
}
data, err := content.GetFileContent()
if err != nil {
return nil, fmt.Errorf("get content: %w", err)
}
return &FileInfo{
Path: filePath,
Ref: ref,
Data: []byte(data),
Hash: content.GetSHA(),
}, nil
}
func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
tree, truncated, err := r.gh.GetTree(ctx, r.owner, r.repo, r.config.Spec.GitHub.Path, ref, true)
if err != nil {
if errors.Is(err, pgh.ErrResourceNotFound) {
return nil, &apierrors.StatusError{
ErrStatus: metav1.Status{
Message: fmt.Sprintf("tree not found; ref=%s", ref),
Code: http.StatusNotFound,
},
}
}
return nil, fmt.Errorf("get tree: %w", err)
}
if truncated {
return nil, fmt.Errorf("tree truncated")
}
entries := make([]FileTreeEntry, 0, len(tree))
for _, entry := range tree {
isBlob := !entry.IsDirectory()
// FIXME: this we could potentially do somewhere else on in a different way
filePath := entry.GetPath()
if !isBlob && !safepath.IsDir(filePath) {
filePath = filePath + "/"
}
converted := FileTreeEntry{
Path: filePath,
Size: entry.GetSize(),
Hash: entry.GetSHA(),
Blob: !entry.IsDirectory(),
}
entries = append(entries, converted)
}
return entries, nil
}
func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
if err := r.ensureBranchExists(ctx, ref); err != nil {
return err
}
finalPath := safepath.Join(r.config.Spec.GitHub.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{}
}
err := r.gh.CreateFile(ctx, r.owner, r.repo, finalPath, ref, comment, data)
if errors.Is(err, pgh.ErrResourceAlreadyExists) {
return &apierrors.StatusError{
ErrStatus: metav1.Status{
Message: "file already exists",
Code: http.StatusConflict,
},
}
}
return err
}
func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
if err := r.ensureBranchExists(ctx, ref); err != nil {
return err
}
finalPath := safepath.Join(r.config.Spec.GitHub.Path, path)
file, _, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref)
if err != nil {
if errors.Is(err, pgh.ErrResourceNotFound) {
return &apierrors.StatusError{
ErrStatus: metav1.Status{
Message: "file not found",
Code: http.StatusNotFound,
},
}
}
return fmt.Errorf("get content before file update: %w", err)
}
if file.IsDirectory() {
return apierrors.NewBadRequest("cannot update a directory")
}
if err := r.gh.UpdateFile(ctx, r.owner, r.repo, finalPath, ref, comment, file.GetSHA(), data); err != nil {
return fmt.Errorf("update file: %w", err)
}
return nil
}
func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
_, err := r.Read(ctx, path, ref)
if err != nil && !(errors.Is(err, 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 *githubRepository) Delete(ctx context.Context, path, ref, comment string) error {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
if err := r.ensureBranchExists(ctx, ref); err != nil {
return err
}
// TODO: should add some protection against deleting the root directory?
// Inside deleteRecursively, all paths are relative to the root of the repository
// so we need to prepend the prefix there but only here.
finalPath := safepath.Join(r.config.Spec.GitHub.Path, path)
return r.deleteRecursively(ctx, finalPath, ref, comment)
}
func (r *githubRepository) deleteRecursively(ctx context.Context, path, ref, comment string) error {
file, contents, err := r.gh.GetContents(ctx, r.owner, r.repo, path, ref)
if err != nil {
if errors.Is(err, pgh.ErrResourceNotFound) {
return ErrFileNotFound
}
return fmt.Errorf("find file to delete: %w", err)
}
if file != nil && !file.IsDirectory() {
return r.gh.DeleteFile(ctx, r.owner, r.repo, path, ref, comment, file.GetSHA())
}
for _, c := range contents {
p := c.GetPath()
if c.IsDirectory() {
if err := r.deleteRecursively(ctx, p, ref, comment); err != nil {
return fmt.Errorf("delete directory recursively: %w", err)
}
continue
}
if err := r.gh.DeleteFile(ctx, r.owner, r.repo, p, ref, comment, c.GetSHA()); err != nil {
return fmt.Errorf("delete file: %w", err)
}
}
return nil
}
func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
finalPath := safepath.Join(r.config.Spec.GitHub.Path, path)
commits, err := r.gh.Commits(ctx, r.owner, r.repo, finalPath, ref)
if err != nil {
if errors.Is(err, pgh.ErrResourceNotFound) {
return nil, ErrFileNotFound
}
return nil, fmt.Errorf("get commits: %w", err)
}
ret := make([]provisioning.HistoryItem, 0, len(commits))
for _, commit := range commits {
authors := make([]provisioning.Author, 0)
if commit.Author != nil {
authors = append(authors, provisioning.Author{
Name: commit.Author.Name,
Username: commit.Author.Username,
AvatarURL: commit.Author.AvatarURL,
})
}
if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name {
authors = append(authors, provisioning.Author{
Name: commit.Committer.Name,
Username: commit.Committer.Username,
AvatarURL: commit.Committer.AvatarURL,
})
}
ret = append(ret, provisioning.HistoryItem{
Ref: commit.Ref,
Message: commit.Message,
Authors: authors,
CreatedAt: commit.CreatedAt.UnixMilli(),
})
}
return ret, nil
}
// basicGitBranchNameRegex is a regular expression to validate a git branch name
// 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.
// 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.
// 3. The branch name cannot contain consecutive slashes (`//`).
// 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 {
if !basicGitBranchNameRegex.MatchString(branch) {
return false
}
// Additional checks for invalid patterns
if strings.HasPrefix(branch, "/") || strings.HasSuffix(branch, "/") ||
strings.HasSuffix(branch, ".") || strings.Contains(branch, "..") ||
strings.Contains(branch, "//") || strings.HasSuffix(branch, ".lock") {
return false
}
return true
}
func (r *githubRepository) ensureBranchExists(ctx context.Context, branchName string) error {
if !IsValidGitBranchName(branchName) {
return &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusBadRequest,
Message: "invalid branch name",
},
}
}
ok, err := r.gh.BranchExists(ctx, r.owner, r.repo, branchName)
if err != nil {
return fmt.Errorf("check branch exists: %w", err)
}
if ok {
logging.FromContext(ctx).Info("branch already exists", "branch", branchName)
return nil
}
srcBranch := r.config.Spec.GitHub.Branch
if err := r.gh.CreateBranch(ctx, r.owner, r.repo, srcBranch, branchName); err != nil {
if errors.Is(err, pgh.ErrResourceAlreadyExists) {
return &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusConflict,
Message: "branch already exists",
},
}
}
return fmt.Errorf("create branch: %w", err)
}
return nil
}
func (r *githubRepository) LatestRef(ctx context.Context) (string, error) {
ctx, _ = r.logger(ctx, "")
branch, err := r.gh.GetBranch(ctx, r.owner, r.repo, r.Config().Spec.GitHub.Branch)
if err != nil {
return "", fmt.Errorf("get branch: %w", err)
}
return branch.Sha, nil
}
func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]VersionedFileChange, error) {
if ref == "" {
var err error
ref, err = r.LatestRef(ctx)
if err != nil {
return nil, fmt.Errorf("get latest ref: %w", err)
}
}
ctx, logger := r.logger(ctx, ref)
files, err := r.gh.CompareCommits(ctx, r.owner, r.repo, base, ref)
if err != nil {
return nil, fmt.Errorf("compare commits: %w", err)
}
changes := make([]VersionedFileChange, 0)
for _, f := range files {
// reference: https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit
switch f.GetStatus() {
case "added", "copied":
currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path)
if err != nil {
// do nothing as it's outside of configured path
continue
}
changes = append(changes, VersionedFileChange{
Path: currentPath,
Ref: ref,
Action: FileActionCreated,
})
case "modified", "changed":
currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path)
if err != nil {
// do nothing as it's outside of configured path
continue
}
changes = append(changes, VersionedFileChange{
Path: currentPath,
Ref: ref,
Action: FileActionUpdated,
})
case "renamed":
previousPath, previousErr := safepath.RelativeTo(f.GetPreviousFilename(), r.config.Spec.GitHub.Path)
currentPath, currentErr := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path)
// Handle all possible combinations of path validation results:
// 1. Both paths outside configured path, do nothing
// 2. Both paths inside configured path, rename
// 3. Moving out of configured path, delete previous file
// 4. Moving into configured path, create new file
switch {
case previousErr != nil && currentErr != nil:
// do nothing as it's outside of configured path
case previousErr == nil && currentErr == nil:
changes = append(changes, VersionedFileChange{
Path: currentPath,
PreviousPath: previousPath,
Ref: ref,
PreviousRef: base,
Action: FileActionRenamed,
})
case previousErr == nil && currentErr != nil:
changes = append(changes, VersionedFileChange{
Path: previousPath,
Ref: base,
Action: FileActionDeleted,
})
case previousErr != nil && currentErr == nil:
changes = append(changes, VersionedFileChange{
Path: currentPath,
Ref: ref,
Action: FileActionCreated,
})
}
case "removed":
currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path)
if err != nil {
// do nothing as it's outside of configured path
continue
}
changes = append(changes, VersionedFileChange{
Ref: ref,
PreviousRef: base,
Path: currentPath,
PreviousPath: currentPath,
Action: FileActionDeleted,
})
case "unchanged":
// do nothing
default:
logger.Error("ignore unhandled file", "file", f.GetFilename(), "status", f.GetStatus())
}
}
return changes, nil
}
// ResourceURLs implements RepositoryWithURLs.
func (r *githubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*provisioning.ResourceURLs, error) {
cfg := r.config.Spec.GitHub
if file.Path == "" || cfg == nil {
return nil, nil
}
ref := file.Ref
if ref == "" {
ref = cfg.Branch
}
urls := &provisioning.ResourceURLs{
RepositoryURL: cfg.URL,
SourceURL: fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path),
}
if ref != cfg.Branch {
urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref)
// Create a new pull request
urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL)
}
return urls, nil
}
func (r *githubRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) {
return r.cloneFn(ctx, opts)
}
func (r *githubRepository) logger(ctx context.Context, ref string) (context.Context, logging.Logger) {
logger := logging.FromContext(ctx)
type containsGh int
var containsGhKey containsGh
if ctx.Value(containsGhKey) != nil {
return ctx, logging.FromContext(ctx)
}
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
logger = logger.With(slog.Group("github_repository", "owner", r.owner, "name", r.repo, "ref", ref))
ctx = logging.Context(ctx, logger)
// We want to ensure we don't add multiple github_repository keys. With doesn't deduplicate the keys...
ctx = context.WithValue(ctx, containsGhKey, true)
return ctx, logger
}

View File

@@ -7,127 +7,34 @@ import (
"errors"
"time"
"github.com/google/go-github/v70/github"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
// API errors that we need to convey after parsing real GH errors (or faking them).
var (
ErrResourceAlreadyExists = errors.New("the resource already exists")
ErrResourceNotFound = errors.New("the resource does not exist")
ErrMismatchedHash = errors.New("the update cannot be applied because the expected and actual hashes are unequal")
ErrNoSecret = errors.New("new webhooks must have a secret")
ErrResourceNotFound = errors.New("the resource does not exist")
//lint:ignore ST1005 this is not punctuation
ErrPathTraversalDisallowed = errors.New("the path contained ..") //nolint:staticcheck
ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable")
ErrFileTooLarge = errors.New("file exceeds maximum allowed size")
ErrTooManyItems = errors.New("maximum number of items exceeded")
ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable")
ErrTooManyItems = errors.New("maximum number of items exceeded")
)
// MaxFileSize maximum file size limit (10MB)
const MaxFileSize = 10 * 1024 * 1024 // 10MB in bytes
type ErrRateLimited = github.RateLimitError
//go:generate mockery --name Client --structname MockClient --inpackage --filename mock_client.go --with-expecter
type Client interface {
// IsAuthenticated checks if the client is authenticated.
IsAuthenticated(ctx context.Context) error
// GetContents returns the metadata and content of a file or directory.
// When a file is checked, the first returned value will have a value. For a directory, the second will. The other value is always nil.
// If an error occurs, the returned values may or may not be nil.
//
// If ".." appears in the "path", this method will return an error.
GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error)
// GetTree returns the Git tree in the repository.
// When recursive is given, subtrees are mapped into the returned array.
// When basePath is given, only trees under it are given. The results do not include this path in their names.
//
// The truncated bool will be set to true if the tree is larger than 7 MB or 100 000 entries.
// When truncated is true, you may wish to read each subtree manually instead.
GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) (entries []RepositoryContent, truncated bool, err error)
// CreateFile creates a new file in the repository under the given path.
// The file is created on the branch given.
// The message given is the commit message. If none is given, an appropriate default is used.
// The content is what the file should contain. An empty slice is valid, though often not very useful.
//
// If ".." appears in the "path", this method will return an error.
CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error
// UpdateFile updates a file in the repository under the given path.
// The file is updated on the branch given.
// The message given is the commit message. If none is given, an appropriate default is used.
// The content is what the file should contain. An empty slice is valid, though often not very useful.
// If the path does not exist, an error is returned.
// The hash given must be the SHA hash of the file contents. Calling GetContents in advance is an easy way of handling this.
//
// If ".." appears in the "path", this method will return an error.
UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error
// DeleteFile deletes a file in the repository under the given path.
// The file is deleted from the branch given.
// The message given is the commit message. If none is given, an appropriate default is used.
// If the path does not exist, an error is returned.
// The hash given must be the SHA hash of the file contents. Calling GetContents in advance is an easy way of handling this.
//
// If ".." appears in the "path", this method will return an error.
DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error
// Commits returns the commits for the given path
// Commits
Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error)
// CompareCommits returns the changes between two commits.
CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error)
// RepoExists checks if a repository exists.
RepoExists(ctx context.Context, owner, repository string) (bool, error)
// CreateBranch creates a new branch in the repository.
CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error
// BranchExists checks if a branch exists in the repository.
BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error)
// GetBranch returns the branch of the repository.
GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error)
// Webhooks
ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error)
CreateWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) (WebhookConfig, error)
GetWebhook(ctx context.Context, owner, repository string, webhookID int64) (WebhookConfig, error)
DeleteWebhook(ctx context.Context, owner, repository string, webhookID int64) error
EditWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) error
// Pull requests
ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error)
CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error
}
//go:generate mockery --name RepositoryContent --structname MockRepositoryContent --inpackage --filename mock_repository_content.go --with-expecter
type RepositoryContent interface {
// Returns true if this is a directory, false if it is a file.
IsDirectory() bool
// Returns the contents of the file. Decoding happens if necessary.
// Returns an error if the content represents a directory.
GetFileContent() (string, error)
// Returns true if this is a symlink.
// If true, GetPath returns the path where this symlink leads.
IsSymlink() bool
// Returns the full path from the root of the repository.
// This has no leading or trailing slashes.
// The path only uses '/' for directories. You can use the 'path' package to interact with these.
GetPath() string
// Get the SHA hash. This is usually a SHA-256, but may also be SHA-512.
// Directories have SHA hashes, too (TODO: how is this calculated?).
GetSHA() string
// The size of the file. Not necessarily non-zero, even if the file is supposed to be non-zero.
GetSize() int64
}
type Branch struct {
Name string
Sha string
}
type CommitAuthor struct {
Name string
Username string
@@ -150,20 +57,6 @@ type CommitFile interface {
GetStatus() string
}
type FileComment struct {
Content string
Path string
Position int
Ref string
}
type CreateFileOptions struct {
// The message of the commit. May be empty, in which case a default value is entered.
Message string
// The content of the file to write, unencoded.
Content []byte
}
type WebhookConfig struct {
// The ID of the webhook.
// Can be 0 on creation.

View File

@@ -27,6 +27,11 @@ func (r *Factory) New(ctx context.Context, ghToken string) Client {
tokenSrc := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: ghToken},
)
tokenClient := oauth2.NewClient(ctx, tokenSrc)
return NewClient(github.NewClient(tokenClient))
if len(ghToken) == 0 {
tokenClient := oauth2.NewClient(ctx, tokenSrc)
return NewClient(github.NewClient(tokenClient))
}
return NewClient(github.NewClient(&http.Client{}))
}

View File

@@ -1,14 +1,14 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository
package github
import (
context "context"
github "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
mock "github.com/stretchr/testify/mock"
field "k8s.io/apimachinery/pkg/util/validation/field"
mock "github.com/stretchr/testify/mock"
repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
@@ -27,19 +27,19 @@ func (_m *MockGithubRepository) EXPECT() *MockGithubRepository_Expecter {
}
// Client provides a mock function with no fields
func (_m *MockGithubRepository) Client() github.Client {
func (_m *MockGithubRepository) Client() 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 {
var r0 Client
if rf, ok := ret.Get(0).(func() Client); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(github.Client)
r0 = ret.Get(0).(Client)
}
}
@@ -63,93 +63,34 @@ func (_c *MockGithubRepository_Client_Call) Run(run func()) *MockGithubRepositor
return _c
}
func (_c *MockGithubRepository_Client_Call) Return(_a0 github.Client) *MockGithubRepository_Client_Call {
func (_c *MockGithubRepository_Client_Call) Return(_a0 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 {
func (_c *MockGithubRepository_Client_Call) RunAndReturn(run func() Client) *MockGithubRepository_Client_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) {
func (_m *MockGithubRepository) CompareFiles(ctx context.Context, base string, ref string) ([]repository.VersionedFileChange, error) {
ret := _m.Called(ctx, base, ref)
if len(ret) == 0 {
panic("no return value specified for CompareFiles")
}
var r0 []VersionedFileChange
var r0 []repository.VersionedFileChange
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]VersionedFileChange, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]repository.VersionedFileChange, error)); ok {
return rf(ctx, base, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) []VersionedFileChange); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string) []repository.VersionedFileChange); ok {
r0 = rf(ctx, base, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]VersionedFileChange)
r0 = ret.Get(0).([]repository.VersionedFileChange)
}
}
@@ -182,12 +123,12 @@ func (_c *MockGithubRepository_CompareFiles_Call) Run(run func(ctx context.Conte
return _c
}
func (_c *MockGithubRepository_CompareFiles_Call) Return(_a0 []VersionedFileChange, _a1 error) *MockGithubRepository_CompareFiles_Call {
func (_c *MockGithubRepository_CompareFiles_Call) Return(_a0 []repository.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 {
func (_c *MockGithubRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]repository.VersionedFileChange, error)) *MockGithubRepository_CompareFiles_Call {
_c.Call.Return(run)
return _c
}
@@ -500,23 +441,23 @@ func (_c *MockGithubRepository_Owner_Call) RunAndReturn(run func() string) *Mock
}
// Read provides a mock function with given fields: ctx, path, ref
func (_m *MockGithubRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) {
func (_m *MockGithubRepository) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) {
ret := _m.Called(ctx, path, ref)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 *FileInfo
var r0 *repository.FileInfo
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string) (*repository.FileInfo, error)); ok {
return rf(ctx, path, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string) *repository.FileInfo); ok {
r0 = rf(ctx, path, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*FileInfo)
r0 = ret.Get(0).(*repository.FileInfo)
}
}
@@ -549,34 +490,34 @@ func (_c *MockGithubRepository_Read_Call) Run(run func(ctx context.Context, path
return _c
}
func (_c *MockGithubRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockGithubRepository_Read_Call {
func (_c *MockGithubRepository_Read_Call) Return(_a0 *repository.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 {
func (_c *MockGithubRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*repository.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) {
func (_m *MockGithubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) {
ret := _m.Called(ctx, ref)
if len(ret) == 0 {
panic("no return value specified for ReadTree")
}
var r0 []FileTreeEntry
var r0 []repository.FileTreeEntry
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, string) ([]repository.FileTreeEntry, error)); ok {
return rf(ctx, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok {
if rf, ok := ret.Get(0).(func(context.Context, string) []repository.FileTreeEntry); ok {
r0 = rf(ctx, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]FileTreeEntry)
r0 = ret.Get(0).([]repository.FileTreeEntry)
}
}
@@ -608,12 +549,12 @@ func (_c *MockGithubRepository_ReadTree_Call) Run(run func(ctx context.Context,
return _c
}
func (_c *MockGithubRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockGithubRepository_ReadTree_Call {
func (_c *MockGithubRepository_ReadTree_Call) Return(_a0 []repository.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 {
func (_c *MockGithubRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]repository.FileTreeEntry, error)) *MockGithubRepository_ReadTree_Call {
_c.Call.Return(run)
return _c
}
@@ -664,7 +605,7 @@ func (_c *MockGithubRepository_Repo_Call) RunAndReturn(run func() string) *MockG
}
// ResourceURLs provides a mock function with given fields: ctx, file
func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*v0alpha1.ResourceURLs, error) {
func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*v0alpha1.ResourceURLs, error) {
ret := _m.Called(ctx, file)
if len(ret) == 0 {
@@ -673,10 +614,10 @@ func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo
var r0 *v0alpha1.ResourceURLs
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *FileInfo) (*v0alpha1.ResourceURLs, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, *repository.FileInfo) (*v0alpha1.ResourceURLs, error)); ok {
return rf(ctx, file)
}
if rf, ok := ret.Get(0).(func(context.Context, *FileInfo) *v0alpha1.ResourceURLs); ok {
if rf, ok := ret.Get(0).(func(context.Context, *repository.FileInfo) *v0alpha1.ResourceURLs); ok {
r0 = rf(ctx, file)
} else {
if ret.Get(0) != nil {
@@ -684,7 +625,7 @@ func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo
}
}
if rf, ok := ret.Get(1).(func(context.Context, *FileInfo) error); ok {
if rf, ok := ret.Get(1).(func(context.Context, *repository.FileInfo) error); ok {
r1 = rf(ctx, file)
} else {
r1 = ret.Error(1)
@@ -700,14 +641,14 @@ type MockGithubRepository_ResourceURLs_Call struct {
// ResourceURLs is a helper method to define mock.On call
// - ctx context.Context
// - file *FileInfo
// - file *repository.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 {
func (_c *MockGithubRepository_ResourceURLs_Call) Run(run func(ctx context.Context, file *repository.FileInfo)) *MockGithubRepository_ResourceURLs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*FileInfo))
run(args[0].(context.Context), args[1].(*repository.FileInfo))
})
return _c
}
@@ -717,7 +658,66 @@ func (_c *MockGithubRepository_ResourceURLs_Call) Return(_a0 *v0alpha1.ResourceU
return _c
}
func (_c *MockGithubRepository_ResourceURLs_Call) RunAndReturn(run func(context.Context, *FileInfo) (*v0alpha1.ResourceURLs, error)) *MockGithubRepository_ResourceURLs_Call {
func (_c *MockGithubRepository_ResourceURLs_Call) RunAndReturn(run func(context.Context, *repository.FileInfo) (*v0alpha1.ResourceURLs, error)) *MockGithubRepository_ResourceURLs_Call {
_c.Call.Return(run)
return _c
}
// Stage provides a mock function with given fields: ctx, opts
func (_m *MockGithubRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) {
ret := _m.Called(ctx, opts)
if len(ret) == 0 {
panic("no return value specified for Stage")
}
var r0 repository.StagedRepository
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) (repository.StagedRepository, error)); ok {
return rf(ctx, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) repository.StagedRepository); ok {
r0 = rf(ctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(repository.StagedRepository)
}
}
if rf, ok := ret.Get(1).(func(context.Context, repository.StageOptions) error); ok {
r1 = rf(ctx, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGithubRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage'
type MockGithubRepository_Stage_Call struct {
*mock.Call
}
// Stage is a helper method to define mock.On call
// - ctx context.Context
// - opts repository.StageOptions
func (_e *MockGithubRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockGithubRepository_Stage_Call {
return &MockGithubRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)}
}
func (_c *MockGithubRepository_Stage_Call) Run(run func(ctx context.Context, opts repository.StageOptions)) *MockGithubRepository_Stage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(repository.StageOptions))
})
return _c
}
func (_c *MockGithubRepository_Stage_Call) Return(_a0 repository.StagedRepository, _a1 error) *MockGithubRepository_Stage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGithubRepository_Stage_Call) RunAndReturn(run func(context.Context, repository.StageOptions) (repository.StagedRepository, error)) *MockGithubRepository_Stage_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -8,10 +8,6 @@ import (
"time"
"github.com/google/go-github/v70/github"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
)
type githubClient struct {
@@ -22,268 +18,12 @@ func NewClient(client *github.Client) Client {
return &githubClient{client}
}
func (r *githubClient) IsAuthenticated(ctx context.Context) error {
if _, _, err := r.gh.Users.Get(ctx, ""); err != nil {
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) {
switch ghErr.Response.StatusCode {
case http.StatusUnauthorized:
return apierrors.NewUnauthorized("token is invalid or expired")
case http.StatusForbidden:
return &apierrors.StatusError{
ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusUnauthorized,
Reason: metav1.StatusReasonUnauthorized,
Message: "token is revoked or has insufficient permissions",
},
}
case http.StatusServiceUnavailable:
return ErrServiceUnavailable
}
}
return err
}
return nil
}
func (r *githubClient) RepoExists(ctx context.Context, owner, repository string) (bool, error) {
_, resp, err := r.gh.Repositories.Get(ctx, owner, repository)
if err == nil {
return true, nil
}
if resp.StatusCode == http.StatusNotFound {
return false, nil
}
return false, err
}
const (
maxDirectoryItems = 1000 // Maximum number of items allowed in a directory
maxTreeItems = 10000 // Maximum number of items allowed in a tree
maxCommits = 1000 // Maximum number of commits to fetch
maxCompareFiles = 1000 // Maximum number of files to compare between commits
maxWebhooks = 100 // Maximum number of webhooks allowed per repository
maxPRFiles = 1000 // Maximum number of files allowed in a pull request
maxPullRequestsFileComments = 1000 // Maximum number of comments allowed in a pull request
maxFileSize = 10 * 1024 * 1024 // 10MB in bytes
maxCommits = 1000 // Maximum number of commits to fetch
maxWebhooks = 100 // Maximum number of webhooks allowed per repository
maxPRFiles = 1000 // Maximum number of files allowed in a pull request
)
func (r *githubClient) GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) {
// First try to get repository contents
opts := &github.RepositoryContentGetOptions{
Ref: ref,
}
fc, dc, _, err := r.gh.Repositories.GetContents(ctx, owner, repository, path, opts)
if err != nil {
var ghErr *github.ErrorResponse
if !errors.As(err, &ghErr) {
return nil, nil, err
}
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
return nil, nil, ErrServiceUnavailable
}
if ghErr.Response.StatusCode == http.StatusNotFound {
return nil, nil, ErrResourceNotFound
}
return nil, nil, err
}
if fc != nil {
// Check file size before returning content
if fc.GetSize() > maxFileSize {
return nil, nil, ErrFileTooLarge
}
return realRepositoryContent{fc}, nil, nil
}
// For directories, check size limits
if len(dc) > maxDirectoryItems {
return nil, nil, fmt.Errorf("directory contains too many items (more than %d)", maxDirectoryItems)
}
// Convert directory contents
allContents := make([]RepositoryContent, 0, len(dc))
for _, original := range dc {
allContents = append(allContents, realRepositoryContent{original})
}
return nil, allContents, nil
}
func (r *githubClient) GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) ([]RepositoryContent, bool, error) {
var tree *github.Tree
var err error
subPaths := safepath.Split(basePath)
currentRef := ref
for {
// If subPaths is empty, we can read recursively, as we're reading the tree from the "base" of the repository. Otherwise, always read only the direct children.
recursive := recursive && len(subPaths) == 0
tree, _, err = r.gh.Git.GetTree(ctx, owner, repository, currentRef, recursive)
if err != nil {
var ghErr *github.ErrorResponse
if !errors.As(err, &ghErr) {
return nil, false, err
}
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
return nil, false, ErrServiceUnavailable
}
if ghErr.Response.StatusCode == http.StatusNotFound {
if currentRef != ref {
// We're operating with a subpath which doesn't exist yet.
// Pretend as if there is simply no files.
// FIXME: why should we pretend this?
return nil, false, nil
}
// currentRef == ref
// This indicates the repository or commitish reference doesn't exist. This should always return an error.
return nil, false, ErrResourceNotFound
}
return nil, false, err
}
// Check if we've exceeded the maximum allowed items
if len(tree.Entries) > maxTreeItems {
return nil, false, fmt.Errorf("tree contains too many items (more than %d)", maxTreeItems)
}
// Prep for next iteration.
if len(subPaths) == 0 {
// We're done: we've discovered the tree we want.
break
}
// the ref must be equal the SHA of the entry corresponding to subPaths[0]
currentRef = ""
for _, e := range tree.Entries {
if e.GetPath() == subPaths[0] {
currentRef = e.GetSHA()
break
}
}
subPaths = subPaths[1:]
if currentRef == "" {
// We couldn't find the folder in the tree...
return nil, false, nil
}
}
// If the tree is truncated and we're in recursive mode, return an error
if tree.GetTruncated() && recursive {
return nil, true, fmt.Errorf("tree is too large to fetch recursively (more than %d items)", maxTreeItems)
}
entries := make([]RepositoryContent, 0, len(tree.Entries))
for _, te := range tree.Entries {
rrc := &realRepositoryContent{
real: &github.RepositoryContent{
Path: te.Path,
Size: te.Size,
SHA: te.SHA,
},
}
if te.GetType() == "tree" {
rrc.real.Type = github.Ptr("dir")
} else {
rrc.real.Type = te.Type
}
entries = append(entries, rrc)
}
return entries, tree.GetTruncated(), nil
}
func (r *githubClient) CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error {
if message == "" {
message = fmt.Sprintf("Create %s", path)
}
_, _, err := r.gh.Repositories.CreateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
Branch: &branch,
Message: &message,
Content: content,
})
if err == nil {
return nil
}
var ghErr *github.ErrorResponse
if !errors.As(err, &ghErr) {
return err
}
if ghErr.Response.StatusCode == http.StatusUnprocessableEntity {
return ErrResourceAlreadyExists
}
return err
}
func (r *githubClient) UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error {
if message == "" {
message = fmt.Sprintf("Update %s", path)
}
_, _, err := r.gh.Repositories.UpdateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
Branch: &branch,
Message: &message,
Content: content,
SHA: &hash,
})
if err == nil {
return nil
}
var ghErr *github.ErrorResponse
if !errors.As(err, &ghErr) {
return err
}
if ghErr.Response.StatusCode == http.StatusNotFound {
return ErrResourceNotFound
}
if ghErr.Response.StatusCode == http.StatusConflict {
return ErrMismatchedHash
}
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
return ErrServiceUnavailable
}
return err
}
func (r *githubClient) DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error {
if message == "" {
message = fmt.Sprintf("Delete %s", path)
}
_, _, err := r.gh.Repositories.DeleteFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
Branch: &branch,
Message: &message,
SHA: &hash,
})
if err == nil {
return nil
}
var ghErr *github.ErrorResponse
if !errors.As(err, &ghErr) {
return err
}
if ghErr.Response.StatusCode == http.StatusNotFound {
return ErrResourceNotFound
}
if ghErr.Response.StatusCode == http.StatusConflict {
return ErrMismatchedHash
}
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
return ErrServiceUnavailable
}
return err
}
// Commits returns a list of commits for a given repository and branch.
func (r *githubClient) Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) {
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.RepositoryCommit, *github.Response, error) {
@@ -343,105 +83,6 @@ func (r *githubClient) Commits(ctx context.Context, owner, repository, path, bra
return ret, nil
}
func (r *githubClient) CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) {
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) {
compare, resp, err := r.gh.Repositories.CompareCommits(ctx, owner, repository, base, head, opts)
if err != nil {
return nil, resp, err
}
return compare.Files, resp, nil
}
files, err := paginatedList(
ctx,
listFn,
defaultListOptions(maxCompareFiles),
)
if errors.Is(err, ErrTooManyItems) {
return nil, fmt.Errorf("too many files changed between commits (more than %d)", maxCompareFiles)
}
if err != nil {
return nil, err
}
// Convert to the interface type
ret := make([]CommitFile, 0, len(files))
for _, f := range files {
ret = append(ret, f)
}
return ret, nil
}
func (r *githubClient) GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) {
branch, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0)
if err != nil {
// For some reason, GitHub client handles this case differently by failing with a wrapped error
if resp != nil && resp.StatusCode == http.StatusNotFound {
return Branch{}, ErrResourceNotFound
}
if resp != nil && resp.StatusCode == http.StatusServiceUnavailable {
return Branch{}, ErrServiceUnavailable
}
var ghErr *github.ErrorResponse
if !errors.As(err, &ghErr) {
return Branch{}, err
}
// Leaving these just in case
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
return Branch{}, ErrServiceUnavailable
}
if ghErr.Response.StatusCode == http.StatusNotFound {
return Branch{}, ErrResourceNotFound
}
return Branch{}, err
}
return Branch{
Name: branch.GetName(),
Sha: branch.GetCommit().GetSHA(),
}, nil
}
func (r *githubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error {
// Fail if the branch already exists
if _, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0); err == nil {
return ErrResourceAlreadyExists
}
// Branch out based on the repository branch
baseRef, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, sourceBranch, 0)
if err != nil {
return fmt.Errorf("get base branch: %w", err)
}
if _, _, err := r.gh.Git.CreateRef(ctx, owner, repository, &github.Reference{
Ref: github.Ptr(fmt.Sprintf("refs/heads/%s", branchName)),
Object: &github.GitObject{
SHA: baseRef.Commit.SHA,
},
}); err != nil {
return fmt.Errorf("create branch ref: %w", err)
}
return nil
}
func (r *githubClient) BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) {
_, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0)
if err == nil {
return true, nil
}
if resp.StatusCode == http.StatusNotFound {
return false, nil
}
return false, err
}
func (r *githubClient) ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) {
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
return r.gh.Repositories.ListHooks(ctx, owner, repository, opts)
@@ -626,44 +267,6 @@ func (r *githubClient) CreatePullRequestComment(ctx context.Context, owner, repo
return nil
}
type realRepositoryContent struct {
real *github.RepositoryContent
}
var _ RepositoryContent = realRepositoryContent{}
func (c realRepositoryContent) IsDirectory() bool {
return c.real.GetType() == "dir"
}
func (c realRepositoryContent) GetFileContent() (string, error) {
return c.real.GetContent()
}
func (c realRepositoryContent) IsSymlink() bool {
return c.real.Target != nil
}
func (c realRepositoryContent) GetPath() string {
return c.real.GetPath()
}
func (c realRepositoryContent) GetSHA() string {
return c.real.GetSHA()
}
func (c realRepositoryContent) GetSize() int64 {
if c.real.Size != nil {
return int64(*c.real.Size)
}
if c.real.Content != nil {
if c, err := c.real.GetContent(); err == nil {
return int64(len(c))
}
}
return 0
}
// listOptions represents pagination parameters for list operations
type listOptions struct {
github.ListOptions

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package github
@@ -21,65 +21,6 @@ func (_m *MockClient) EXPECT() *MockClient_Expecter {
return &MockClient_Expecter{mock: &_m.Mock}
}
// BranchExists provides a mock function with given fields: ctx, owner, repository, branchName
func (_m *MockClient) BranchExists(ctx context.Context, owner string, repository string, branchName string) (bool, error) {
ret := _m.Called(ctx, owner, repository, branchName)
if len(ret) == 0 {
panic("no return value specified for BranchExists")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (bool, error)); ok {
return rf(ctx, owner, repository, branchName)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) bool); ok {
r0 = rf(ctx, owner, repository, branchName)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
r1 = rf(ctx, owner, repository, branchName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockClient_BranchExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BranchExists'
type MockClient_BranchExists_Call struct {
*mock.Call
}
// BranchExists is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - branchName string
func (_e *MockClient_Expecter) BranchExists(ctx interface{}, owner interface{}, repository interface{}, branchName interface{}) *MockClient_BranchExists_Call {
return &MockClient_BranchExists_Call{Call: _e.mock.On("BranchExists", ctx, owner, repository, branchName)}
}
func (_c *MockClient_BranchExists_Call) Run(run func(ctx context.Context, owner string, repository string, branchName string)) *MockClient_BranchExists_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 *MockClient_BranchExists_Call) Return(_a0 bool, _a1 error) *MockClient_BranchExists_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClient_BranchExists_Call) RunAndReturn(run func(context.Context, string, string, string) (bool, error)) *MockClient_BranchExists_Call {
_c.Call.Return(run)
return _c
}
// Commits provides a mock function with given fields: ctx, owner, repository, path, branch
func (_m *MockClient) Commits(ctx context.Context, owner string, repository string, path string, branch string) ([]Commit, error) {
ret := _m.Called(ctx, owner, repository, path, branch)
@@ -142,170 +83,6 @@ func (_c *MockClient_Commits_Call) RunAndReturn(run func(context.Context, string
return _c
}
// CompareCommits provides a mock function with given fields: ctx, owner, repository, base, head
func (_m *MockClient) CompareCommits(ctx context.Context, owner string, repository string, base string, head string) ([]CommitFile, error) {
ret := _m.Called(ctx, owner, repository, base, head)
if len(ret) == 0 {
panic("no return value specified for CompareCommits")
}
var r0 []CommitFile
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]CommitFile, error)); ok {
return rf(ctx, owner, repository, base, head)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []CommitFile); ok {
r0 = rf(ctx, owner, repository, base, head)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]CommitFile)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok {
r1 = rf(ctx, owner, repository, base, head)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockClient_CompareCommits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompareCommits'
type MockClient_CompareCommits_Call struct {
*mock.Call
}
// CompareCommits is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - base string
// - head string
func (_e *MockClient_Expecter) CompareCommits(ctx interface{}, owner interface{}, repository interface{}, base interface{}, head interface{}) *MockClient_CompareCommits_Call {
return &MockClient_CompareCommits_Call{Call: _e.mock.On("CompareCommits", ctx, owner, repository, base, head)}
}
func (_c *MockClient_CompareCommits_Call) Run(run func(ctx context.Context, owner string, repository string, base string, head string)) *MockClient_CompareCommits_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string))
})
return _c
}
func (_c *MockClient_CompareCommits_Call) Return(_a0 []CommitFile, _a1 error) *MockClient_CompareCommits_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClient_CompareCommits_Call) RunAndReturn(run func(context.Context, string, string, string, string) ([]CommitFile, error)) *MockClient_CompareCommits_Call {
_c.Call.Return(run)
return _c
}
// CreateBranch provides a mock function with given fields: ctx, owner, repository, sourceBranch, branchName
func (_m *MockClient) CreateBranch(ctx context.Context, owner string, repository string, sourceBranch string, branchName string) error {
ret := _m.Called(ctx, owner, repository, sourceBranch, branchName)
if len(ret) == 0 {
panic("no return value specified for CreateBranch")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok {
r0 = rf(ctx, owner, repository, sourceBranch, branchName)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockClient_CreateBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBranch'
type MockClient_CreateBranch_Call struct {
*mock.Call
}
// CreateBranch is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - sourceBranch string
// - branchName string
func (_e *MockClient_Expecter) CreateBranch(ctx interface{}, owner interface{}, repository interface{}, sourceBranch interface{}, branchName interface{}) *MockClient_CreateBranch_Call {
return &MockClient_CreateBranch_Call{Call: _e.mock.On("CreateBranch", ctx, owner, repository, sourceBranch, branchName)}
}
func (_c *MockClient_CreateBranch_Call) Run(run func(ctx context.Context, owner string, repository string, sourceBranch string, branchName string)) *MockClient_CreateBranch_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string))
})
return _c
}
func (_c *MockClient_CreateBranch_Call) Return(_a0 error) *MockClient_CreateBranch_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClient_CreateBranch_Call) RunAndReturn(run func(context.Context, string, string, string, string) error) *MockClient_CreateBranch_Call {
_c.Call.Return(run)
return _c
}
// CreateFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, content
func (_m *MockClient) CreateFile(ctx context.Context, owner string, repository string, path string, branch string, message string, content []byte) error {
ret := _m.Called(ctx, owner, repository, path, branch, message, content)
if len(ret) == 0 {
panic("no return value specified for CreateFile")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, []byte) error); ok {
r0 = rf(ctx, owner, repository, path, branch, message, content)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockClient_CreateFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateFile'
type MockClient_CreateFile_Call struct {
*mock.Call
}
// CreateFile is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - path string
// - branch string
// - message string
// - content []byte
func (_e *MockClient_Expecter) CreateFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, content interface{}) *MockClient_CreateFile_Call {
return &MockClient_CreateFile_Call{Call: _e.mock.On("CreateFile", ctx, owner, repository, path, branch, message, content)}
}
func (_c *MockClient_CreateFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, content []byte)) *MockClient_CreateFile_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].([]byte))
})
return _c
}
func (_c *MockClient_CreateFile_Call) Return(_a0 error) *MockClient_CreateFile_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClient_CreateFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, []byte) error) *MockClient_CreateFile_Call {
_c.Call.Return(run)
return _c
}
// CreatePullRequestComment provides a mock function with given fields: ctx, owner, repository, number, body
func (_m *MockClient) CreatePullRequestComment(ctx context.Context, owner string, repository string, number int, body string) error {
ret := _m.Called(ctx, owner, repository, number, body)
@@ -415,58 +192,6 @@ func (_c *MockClient_CreateWebhook_Call) RunAndReturn(run func(context.Context,
return _c
}
// DeleteFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, hash
func (_m *MockClient) DeleteFile(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string) error {
ret := _m.Called(ctx, owner, repository, path, branch, message, hash)
if len(ret) == 0 {
panic("no return value specified for DeleteFile")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string) error); ok {
r0 = rf(ctx, owner, repository, path, branch, message, hash)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockClient_DeleteFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteFile'
type MockClient_DeleteFile_Call struct {
*mock.Call
}
// DeleteFile is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - path string
// - branch string
// - message string
// - hash string
func (_e *MockClient_Expecter) DeleteFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, hash interface{}) *MockClient_DeleteFile_Call {
return &MockClient_DeleteFile_Call{Call: _e.mock.On("DeleteFile", ctx, owner, repository, path, branch, message, hash)}
}
func (_c *MockClient_DeleteFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string)) *MockClient_DeleteFile_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string))
})
return _c
}
func (_c *MockClient_DeleteFile_Call) Return(_a0 error) *MockClient_DeleteFile_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClient_DeleteFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, string) error) *MockClient_DeleteFile_Call {
_c.Call.Return(run)
return _c
}
// DeleteWebhook provides a mock function with given fields: ctx, owner, repository, webhookID
func (_m *MockClient) DeleteWebhook(ctx context.Context, owner string, repository string, webhookID int64) error {
ret := _m.Called(ctx, owner, repository, webhookID)
@@ -565,206 +290,6 @@ func (_c *MockClient_EditWebhook_Call) RunAndReturn(run func(context.Context, st
return _c
}
// GetBranch provides a mock function with given fields: ctx, owner, repository, branchName
func (_m *MockClient) GetBranch(ctx context.Context, owner string, repository string, branchName string) (Branch, error) {
ret := _m.Called(ctx, owner, repository, branchName)
if len(ret) == 0 {
panic("no return value specified for GetBranch")
}
var r0 Branch
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (Branch, error)); ok {
return rf(ctx, owner, repository, branchName)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) Branch); ok {
r0 = rf(ctx, owner, repository, branchName)
} else {
r0 = ret.Get(0).(Branch)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
r1 = rf(ctx, owner, repository, branchName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockClient_GetBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBranch'
type MockClient_GetBranch_Call struct {
*mock.Call
}
// GetBranch is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - branchName string
func (_e *MockClient_Expecter) GetBranch(ctx interface{}, owner interface{}, repository interface{}, branchName interface{}) *MockClient_GetBranch_Call {
return &MockClient_GetBranch_Call{Call: _e.mock.On("GetBranch", ctx, owner, repository, branchName)}
}
func (_c *MockClient_GetBranch_Call) Run(run func(ctx context.Context, owner string, repository string, branchName string)) *MockClient_GetBranch_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 *MockClient_GetBranch_Call) Return(_a0 Branch, _a1 error) *MockClient_GetBranch_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClient_GetBranch_Call) RunAndReturn(run func(context.Context, string, string, string) (Branch, error)) *MockClient_GetBranch_Call {
_c.Call.Return(run)
return _c
}
// GetContents provides a mock function with given fields: ctx, owner, repository, path, ref
func (_m *MockClient) GetContents(ctx context.Context, owner string, repository string, path string, ref string) (RepositoryContent, []RepositoryContent, error) {
ret := _m.Called(ctx, owner, repository, path, ref)
if len(ret) == 0 {
panic("no return value specified for GetContents")
}
var r0 RepositoryContent
var r1 []RepositoryContent
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (RepositoryContent, []RepositoryContent, error)); ok {
return rf(ctx, owner, repository, path, ref)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) RepositoryContent); ok {
r0 = rf(ctx, owner, repository, path, ref)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(RepositoryContent)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) []RepositoryContent); ok {
r1 = rf(ctx, owner, repository, path, ref)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]RepositoryContent)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string) error); ok {
r2 = rf(ctx, owner, repository, path, ref)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// MockClient_GetContents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContents'
type MockClient_GetContents_Call struct {
*mock.Call
}
// GetContents is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - path string
// - ref string
func (_e *MockClient_Expecter) GetContents(ctx interface{}, owner interface{}, repository interface{}, path interface{}, ref interface{}) *MockClient_GetContents_Call {
return &MockClient_GetContents_Call{Call: _e.mock.On("GetContents", ctx, owner, repository, path, ref)}
}
func (_c *MockClient_GetContents_Call) Run(run func(ctx context.Context, owner string, repository string, path string, ref string)) *MockClient_GetContents_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string))
})
return _c
}
func (_c *MockClient_GetContents_Call) Return(fileContents RepositoryContent, dirContents []RepositoryContent, err error) *MockClient_GetContents_Call {
_c.Call.Return(fileContents, dirContents, err)
return _c
}
func (_c *MockClient_GetContents_Call) RunAndReturn(run func(context.Context, string, string, string, string) (RepositoryContent, []RepositoryContent, error)) *MockClient_GetContents_Call {
_c.Call.Return(run)
return _c
}
// GetTree provides a mock function with given fields: ctx, owner, repository, basePath, ref, recursive
func (_m *MockClient) GetTree(ctx context.Context, owner string, repository string, basePath string, ref string, recursive bool) ([]RepositoryContent, bool, error) {
ret := _m.Called(ctx, owner, repository, basePath, ref, recursive)
if len(ret) == 0 {
panic("no return value specified for GetTree")
}
var r0 []RepositoryContent
var r1 bool
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool) ([]RepositoryContent, bool, error)); ok {
return rf(ctx, owner, repository, basePath, ref, recursive)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool) []RepositoryContent); ok {
r0 = rf(ctx, owner, repository, basePath, ref, recursive)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]RepositoryContent)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool) bool); ok {
r1 = rf(ctx, owner, repository, basePath, ref, recursive)
} else {
r1 = ret.Get(1).(bool)
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string, bool) error); ok {
r2 = rf(ctx, owner, repository, basePath, ref, recursive)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// MockClient_GetTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTree'
type MockClient_GetTree_Call struct {
*mock.Call
}
// GetTree is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - basePath string
// - ref string
// - recursive bool
func (_e *MockClient_Expecter) GetTree(ctx interface{}, owner interface{}, repository interface{}, basePath interface{}, ref interface{}, recursive interface{}) *MockClient_GetTree_Call {
return &MockClient_GetTree_Call{Call: _e.mock.On("GetTree", ctx, owner, repository, basePath, ref, recursive)}
}
func (_c *MockClient_GetTree_Call) Run(run func(ctx context.Context, owner string, repository string, basePath string, ref string, recursive bool)) *MockClient_GetTree_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(bool))
})
return _c
}
func (_c *MockClient_GetTree_Call) Return(entries []RepositoryContent, truncated bool, err error) *MockClient_GetTree_Call {
_c.Call.Return(entries, truncated, err)
return _c
}
func (_c *MockClient_GetTree_Call) RunAndReturn(run func(context.Context, string, string, string, string, bool) ([]RepositoryContent, bool, error)) *MockClient_GetTree_Call {
_c.Call.Return(run)
return _c
}
// GetWebhook provides a mock function with given fields: ctx, owner, repository, webhookID
func (_m *MockClient) GetWebhook(ctx context.Context, owner string, repository string, webhookID int64) (WebhookConfig, error) {
ret := _m.Called(ctx, owner, repository, webhookID)
@@ -824,52 +349,6 @@ func (_c *MockClient_GetWebhook_Call) RunAndReturn(run func(context.Context, str
return _c
}
// IsAuthenticated provides a mock function with given fields: ctx
func (_m *MockClient) IsAuthenticated(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for IsAuthenticated")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockClient_IsAuthenticated_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAuthenticated'
type MockClient_IsAuthenticated_Call struct {
*mock.Call
}
// IsAuthenticated is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockClient_Expecter) IsAuthenticated(ctx interface{}) *MockClient_IsAuthenticated_Call {
return &MockClient_IsAuthenticated_Call{Call: _e.mock.On("IsAuthenticated", ctx)}
}
func (_c *MockClient_IsAuthenticated_Call) Run(run func(ctx context.Context)) *MockClient_IsAuthenticated_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockClient_IsAuthenticated_Call) Return(_a0 error) *MockClient_IsAuthenticated_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClient_IsAuthenticated_Call) RunAndReturn(run func(context.Context) error) *MockClient_IsAuthenticated_Call {
_c.Call.Return(run)
return _c
}
// ListPullRequestFiles provides a mock function with given fields: ctx, owner, repository, number
func (_m *MockClient) ListPullRequestFiles(ctx context.Context, owner string, repository string, number int) ([]CommitFile, error) {
ret := _m.Called(ctx, owner, repository, number)
@@ -991,117 +470,6 @@ func (_c *MockClient_ListWebhooks_Call) RunAndReturn(run func(context.Context, s
return _c
}
// RepoExists provides a mock function with given fields: ctx, owner, repository
func (_m *MockClient) RepoExists(ctx context.Context, owner string, repository string) (bool, error) {
ret := _m.Called(ctx, owner, repository)
if len(ret) == 0 {
panic("no return value specified for RepoExists")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok {
return rf(ctx, owner, repository)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok {
r0 = rf(ctx, owner, repository)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, owner, repository)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockClient_RepoExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RepoExists'
type MockClient_RepoExists_Call struct {
*mock.Call
}
// RepoExists is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
func (_e *MockClient_Expecter) RepoExists(ctx interface{}, owner interface{}, repository interface{}) *MockClient_RepoExists_Call {
return &MockClient_RepoExists_Call{Call: _e.mock.On("RepoExists", ctx, owner, repository)}
}
func (_c *MockClient_RepoExists_Call) Run(run func(ctx context.Context, owner string, repository string)) *MockClient_RepoExists_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockClient_RepoExists_Call) Return(_a0 bool, _a1 error) *MockClient_RepoExists_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClient_RepoExists_Call) RunAndReturn(run func(context.Context, string, string) (bool, error)) *MockClient_RepoExists_Call {
_c.Call.Return(run)
return _c
}
// UpdateFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, hash, content
func (_m *MockClient) UpdateFile(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string, content []byte) error {
ret := _m.Called(ctx, owner, repository, path, branch, message, hash, content)
if len(ret) == 0 {
panic("no return value specified for UpdateFile")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string, []byte) error); ok {
r0 = rf(ctx, owner, repository, path, branch, message, hash, content)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockClient_UpdateFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateFile'
type MockClient_UpdateFile_Call struct {
*mock.Call
}
// UpdateFile is a helper method to define mock.On call
// - ctx context.Context
// - owner string
// - repository string
// - path string
// - branch string
// - message string
// - hash string
// - content []byte
func (_e *MockClient_Expecter) UpdateFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, hash interface{}, content interface{}) *MockClient_UpdateFile_Call {
return &MockClient_UpdateFile_Call{Call: _e.mock.On("UpdateFile", ctx, owner, repository, path, branch, message, hash, content)}
}
func (_c *MockClient_UpdateFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string, content []byte)) *MockClient_UpdateFile_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string), args[7].([]byte))
})
return _c
}
func (_c *MockClient_UpdateFile_Call) Return(_a0 error) *MockClient_UpdateFile_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClient_UpdateFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, string, []byte) error) *MockClient_UpdateFile_Call {
_c.Call.Return(run)
return _c
}
// NewMockClient creates a new instance of MockClient. 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 NewMockClient(t interface {

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package github

View File

@@ -1,312 +0,0 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
package github
import mock "github.com/stretchr/testify/mock"
// MockRepositoryContent is an autogenerated mock type for the RepositoryContent type
type MockRepositoryContent struct {
mock.Mock
}
type MockRepositoryContent_Expecter struct {
mock *mock.Mock
}
func (_m *MockRepositoryContent) EXPECT() *MockRepositoryContent_Expecter {
return &MockRepositoryContent_Expecter{mock: &_m.Mock}
}
// GetFileContent provides a mock function with no fields
func (_m *MockRepositoryContent) GetFileContent() (string, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetFileContent")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func() (string, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockRepositoryContent_GetFileContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFileContent'
type MockRepositoryContent_GetFileContent_Call struct {
*mock.Call
}
// GetFileContent is a helper method to define mock.On call
func (_e *MockRepositoryContent_Expecter) GetFileContent() *MockRepositoryContent_GetFileContent_Call {
return &MockRepositoryContent_GetFileContent_Call{Call: _e.mock.On("GetFileContent")}
}
func (_c *MockRepositoryContent_GetFileContent_Call) Run(run func()) *MockRepositoryContent_GetFileContent_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockRepositoryContent_GetFileContent_Call) Return(_a0 string, _a1 error) *MockRepositoryContent_GetFileContent_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockRepositoryContent_GetFileContent_Call) RunAndReturn(run func() (string, error)) *MockRepositoryContent_GetFileContent_Call {
_c.Call.Return(run)
return _c
}
// GetPath provides a mock function with no fields
func (_m *MockRepositoryContent) GetPath() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetPath")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockRepositoryContent_GetPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPath'
type MockRepositoryContent_GetPath_Call struct {
*mock.Call
}
// GetPath is a helper method to define mock.On call
func (_e *MockRepositoryContent_Expecter) GetPath() *MockRepositoryContent_GetPath_Call {
return &MockRepositoryContent_GetPath_Call{Call: _e.mock.On("GetPath")}
}
func (_c *MockRepositoryContent_GetPath_Call) Run(run func()) *MockRepositoryContent_GetPath_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockRepositoryContent_GetPath_Call) Return(_a0 string) *MockRepositoryContent_GetPath_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRepositoryContent_GetPath_Call) RunAndReturn(run func() string) *MockRepositoryContent_GetPath_Call {
_c.Call.Return(run)
return _c
}
// GetSHA provides a mock function with no fields
func (_m *MockRepositoryContent) GetSHA() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetSHA")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockRepositoryContent_GetSHA_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSHA'
type MockRepositoryContent_GetSHA_Call struct {
*mock.Call
}
// GetSHA is a helper method to define mock.On call
func (_e *MockRepositoryContent_Expecter) GetSHA() *MockRepositoryContent_GetSHA_Call {
return &MockRepositoryContent_GetSHA_Call{Call: _e.mock.On("GetSHA")}
}
func (_c *MockRepositoryContent_GetSHA_Call) Run(run func()) *MockRepositoryContent_GetSHA_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockRepositoryContent_GetSHA_Call) Return(_a0 string) *MockRepositoryContent_GetSHA_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRepositoryContent_GetSHA_Call) RunAndReturn(run func() string) *MockRepositoryContent_GetSHA_Call {
_c.Call.Return(run)
return _c
}
// GetSize provides a mock function with no fields
func (_m *MockRepositoryContent) GetSize() int64 {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetSize")
}
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
return r0
}
// MockRepositoryContent_GetSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSize'
type MockRepositoryContent_GetSize_Call struct {
*mock.Call
}
// GetSize is a helper method to define mock.On call
func (_e *MockRepositoryContent_Expecter) GetSize() *MockRepositoryContent_GetSize_Call {
return &MockRepositoryContent_GetSize_Call{Call: _e.mock.On("GetSize")}
}
func (_c *MockRepositoryContent_GetSize_Call) Run(run func()) *MockRepositoryContent_GetSize_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockRepositoryContent_GetSize_Call) Return(_a0 int64) *MockRepositoryContent_GetSize_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRepositoryContent_GetSize_Call) RunAndReturn(run func() int64) *MockRepositoryContent_GetSize_Call {
_c.Call.Return(run)
return _c
}
// IsDirectory provides a mock function with no fields
func (_m *MockRepositoryContent) IsDirectory() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for IsDirectory")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockRepositoryContent_IsDirectory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsDirectory'
type MockRepositoryContent_IsDirectory_Call struct {
*mock.Call
}
// IsDirectory is a helper method to define mock.On call
func (_e *MockRepositoryContent_Expecter) IsDirectory() *MockRepositoryContent_IsDirectory_Call {
return &MockRepositoryContent_IsDirectory_Call{Call: _e.mock.On("IsDirectory")}
}
func (_c *MockRepositoryContent_IsDirectory_Call) Run(run func()) *MockRepositoryContent_IsDirectory_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockRepositoryContent_IsDirectory_Call) Return(_a0 bool) *MockRepositoryContent_IsDirectory_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRepositoryContent_IsDirectory_Call) RunAndReturn(run func() bool) *MockRepositoryContent_IsDirectory_Call {
_c.Call.Return(run)
return _c
}
// IsSymlink provides a mock function with no fields
func (_m *MockRepositoryContent) IsSymlink() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for IsSymlink")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockRepositoryContent_IsSymlink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsSymlink'
type MockRepositoryContent_IsSymlink_Call struct {
*mock.Call
}
// IsSymlink is a helper method to define mock.On call
func (_e *MockRepositoryContent_Expecter) IsSymlink() *MockRepositoryContent_IsSymlink_Call {
return &MockRepositoryContent_IsSymlink_Call{Call: _e.mock.On("IsSymlink")}
}
func (_c *MockRepositoryContent_IsSymlink_Call) Run(run func()) *MockRepositoryContent_IsSymlink_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockRepositoryContent_IsSymlink_Call) Return(_a0 bool) *MockRepositoryContent_IsSymlink_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRepositoryContent_IsSymlink_Call) RunAndReturn(run func() bool) *MockRepositoryContent_IsSymlink_Call {
_c.Call.Return(run)
return _c
}
// NewMockRepositoryContent creates a new instance of MockRepositoryContent. 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 NewMockRepositoryContent(t interface {
mock.TestingT
Cleanup(func())
}) *MockRepositoryContent {
mock := &MockRepositoryContent{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +0,0 @@
package gogit
import (
"bufio"
"bytes"
"io"
)
func Progress(lines func(line string), final string) io.WriteCloser {
reader, writer := io.Pipe()
scanner := bufio.NewScanner(reader)
scanner.Split(scanLines)
go func() {
for scanner.Scan() {
line := scanner.Text()
if line != "" {
lines(line)
}
}
lines(final)
}()
return writer
}
// Copied from bufio.ScanLines and modifed to accept standalone \r as input
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\r'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0:i], nil
}
// Support standalone newlines also
if i := bytes.IndexByte(data, '\n'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}

View File

@@ -1,58 +0,0 @@
package gogit
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProgressParsing(t *testing.T) {
tests := []struct {
name string
input string
expect []string
}{
{
name: "no breaks",
input: "some text",
expect: []string{"some text"},
},
{
name: "with cr",
input: "hello\rworld",
expect: []string{"hello", "world"},
},
{
name: "with nl",
input: "hello\nworld",
expect: []string{"hello", "world"},
},
{
name: "with cr+nl",
input: "hello\r\nworld",
expect: []string{"hello", "world"},
},
}
for _, tt := range tests {
lastLine := "***LAST*LINE***"
t.Run(tt.name, func(t *testing.T) {
lines := []string{}
writer := Progress(func(line string) {
lines = append(lines, line)
}, lastLine)
_, _ = writer.Write([]byte(tt.input))
err := writer.Close()
require.NoError(t, err)
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.NotEmpty(c, lines)
assert.Equal(c, lastLine, lines[len(lines)-1])
// Compare the results
require.Equal(c, tt.expect, lines[0:len(lines)-1])
}, time.Millisecond*100, time.Microsecond*50)
})
}
}

View File

@@ -1,84 +0,0 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
package gogit
import (
context "context"
git "github.com/go-git/go-git/v5"
mock "github.com/stretchr/testify/mock"
)
// MockRepository is an autogenerated mock type for the Repository type
type MockRepository struct {
mock.Mock
}
type MockRepository_Expecter struct {
mock *mock.Mock
}
func (_m *MockRepository) EXPECT() *MockRepository_Expecter {
return &MockRepository_Expecter{mock: &_m.Mock}
}
// PushContext provides a mock function with given fields: ctx, o
func (_m *MockRepository) PushContext(ctx context.Context, o *git.PushOptions) error {
ret := _m.Called(ctx, o)
if len(ret) == 0 {
panic("no return value specified for PushContext")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *git.PushOptions) error); ok {
r0 = rf(ctx, o)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockRepository_PushContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PushContext'
type MockRepository_PushContext_Call struct {
*mock.Call
}
// PushContext is a helper method to define mock.On call
// - ctx context.Context
// - o *git.PushOptions
func (_e *MockRepository_Expecter) PushContext(ctx interface{}, o interface{}) *MockRepository_PushContext_Call {
return &MockRepository_PushContext_Call{Call: _e.mock.On("PushContext", ctx, o)}
}
func (_c *MockRepository_PushContext_Call) Run(run func(ctx context.Context, o *git.PushOptions)) *MockRepository_PushContext_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*git.PushOptions))
})
return _c
}
func (_c *MockRepository_PushContext_Call) Return(_a0 error) *MockRepository_PushContext_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRepository_PushContext_Call) RunAndReturn(run func(context.Context, *git.PushOptions) error) *MockRepository_PushContext_Call {
_c.Call.Return(run)
return _c
}
// NewMockRepository creates a new instance of MockRepository. 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 NewMockRepository(t interface {
mock.TestingT
Cleanup(func())
}) *MockRepository {
mock := &MockRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,73 +0,0 @@
package gogit
import (
"fmt"
"io"
"net/http"
"sync/atomic"
"github.com/grafana/grafana/pkg/util/httpclient"
)
var errBytesLimitExceeded = fmt.Errorf("bytes limit exceeded")
// ByteLimitedTransport wraps http.RoundTripper to enforce a max byte limit
type ByteLimitedTransport struct {
Transport http.RoundTripper
Limit int64
Bytes int64
}
// NewByteLimitedTransport creates a new ByteLimitedTransport with the specified transport and byte limit.
// If transport is nil, a new http.Transport modeled after http.DefaultTransport will be used.
func NewByteLimitedTransport(transport http.RoundTripper, limit int64) *ByteLimitedTransport {
if transport == nil {
transport = httpclient.NewHTTPTransport()
}
return &ByteLimitedTransport{
Transport: transport,
Limit: limit,
Bytes: 0,
}
}
// RoundTrip tracks downloaded bytes and aborts if limit is exceeded
func (b *ByteLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := b.Transport.RoundTrip(req)
if err != nil {
return nil, err
}
// Wrap response body to track bytes read
resp.Body = &byteLimitedReader{
reader: resp.Body,
limit: b.Limit,
bytes: &b.Bytes,
}
return resp, nil
}
// byteLimitedReader tracks and enforces a download limit
type byteLimitedReader struct {
reader io.ReadCloser
limit int64
bytes *int64
}
func (r *byteLimitedReader) Read(p []byte) (int, error) {
n, err := r.reader.Read(p)
if err != nil {
return n, err
}
if atomic.AddInt64(r.bytes, int64(n)) > r.limit {
return 0, errBytesLimitExceeded
}
return n, nil
}
func (r *byteLimitedReader) Close() error {
return r.reader.Close()
}

View File

@@ -1,140 +0,0 @@
package gogit
import (
"bytes"
"errors"
"io"
"net/http"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockTransport struct {
response *http.Response
err error
}
func (m *mockTransport) RoundTrip(*http.Request) (*http.Response, error) {
return m.response, m.err
}
func TestNewByteLimitedTransport(t *testing.T) {
tests := []struct {
name string
transport http.RoundTripper
limit int64
}{
{
name: "with custom transport",
transport: &mockTransport{},
limit: 1000,
},
{
name: "with nil transport",
transport: nil,
limit: 1000,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blt := NewByteLimitedTransport(tt.transport, tt.limit)
assert.NotNil(t, blt)
assert.Equal(t, tt.limit, blt.Limit)
assert.Equal(t, int64(0), blt.Bytes)
if tt.transport == nil {
assert.NotNil(t, blt.Transport)
assert.NotEqual(t, http.DefaultTransport, blt.Transport)
} else {
assert.Equal(t, tt.transport, blt.Transport)
}
})
}
}
func TestByteLimitedTransport_RoundTrip(t *testing.T) {
tests := []struct {
name string
responseBody string
limit int64
expectedError error
}{
{
name: "under limit",
responseBody: "small response",
limit: 100,
expectedError: nil,
},
{
name: "exceeds limit",
responseBody: "this response will exceed the byte limit",
limit: 10,
expectedError: errBytesLimitExceeded,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockResp := &http.Response{
Body: io.NopCloser(bytes.NewBufferString(tt.responseBody)),
}
mockTransport := &mockTransport{response: mockResp}
blt := NewByteLimitedTransport(mockTransport, tt.limit)
resp, err := blt.RoundTrip(&http.Request{})
require.NoError(t, err)
defer func() {
closeErr := resp.Body.Close()
assert.NoError(t, closeErr, "failed to close response body")
}()
data, err := io.ReadAll(resp.Body)
if tt.expectedError != nil {
assert.True(t, errors.Is(err, tt.expectedError), "expected error %v, got %v", tt.expectedError, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.responseBody, string(data))
}
})
}
}
func TestByteLimitedReader_Close(t *testing.T) {
mockBody := io.NopCloser(bytes.NewBufferString("test"))
var byteCount int64
reader := &byteLimitedReader{
reader: mockBody,
limit: 100,
bytes: &byteCount,
}
err := reader.Close()
assert.NoError(t, err)
}
func TestByteLimitedReader_AtomicCounting(t *testing.T) {
var byteCount int64
reader := &byteLimitedReader{
reader: io.NopCloser(bytes.NewBufferString("test data")),
limit: 5,
bytes: &byteCount,
}
// First read should succeed
buf := make([]byte, 4)
n, err := reader.Read(buf)
assert.NoError(t, err)
assert.Equal(t, 4, n)
// Second read should fail due to limit
n, err = reader.Read(buf)
assert.True(t, errors.Is(err, errBytesLimitExceeded), "expected error %v, got %v", errBytesLimitExceeded, err)
assert.Equal(t, 0, n)
// Verify atomic counter
assert.Greater(t, atomic.LoadInt64(&byteCount), int64(5))
}

View File

@@ -1,261 +0,0 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
package gogit
import (
billy "github.com/go-git/go-billy/v5"
git "github.com/go-git/go-git/v5"
mock "github.com/stretchr/testify/mock"
plumbing "github.com/go-git/go-git/v5/plumbing"
)
// MockWorktree is an autogenerated mock type for the Worktree type
type MockWorktree struct {
mock.Mock
}
type MockWorktree_Expecter struct {
mock *mock.Mock
}
func (_m *MockWorktree) EXPECT() *MockWorktree_Expecter {
return &MockWorktree_Expecter{mock: &_m.Mock}
}
// Add provides a mock function with given fields: path
func (_m *MockWorktree) Add(path string) (plumbing.Hash, error) {
ret := _m.Called(path)
if len(ret) == 0 {
panic("no return value specified for Add")
}
var r0 plumbing.Hash
var r1 error
if rf, ok := ret.Get(0).(func(string) (plumbing.Hash, error)); ok {
return rf(path)
}
if rf, ok := ret.Get(0).(func(string) plumbing.Hash); ok {
r0 = rf(path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(plumbing.Hash)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockWorktree_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add'
type MockWorktree_Add_Call struct {
*mock.Call
}
// Add is a helper method to define mock.On call
// - path string
func (_e *MockWorktree_Expecter) Add(path interface{}) *MockWorktree_Add_Call {
return &MockWorktree_Add_Call{Call: _e.mock.On("Add", path)}
}
func (_c *MockWorktree_Add_Call) Run(run func(path string)) *MockWorktree_Add_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockWorktree_Add_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Add_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockWorktree_Add_Call) RunAndReturn(run func(string) (plumbing.Hash, error)) *MockWorktree_Add_Call {
_c.Call.Return(run)
return _c
}
// Commit provides a mock function with given fields: message, opts
func (_m *MockWorktree) Commit(message string, opts *git.CommitOptions) (plumbing.Hash, error) {
ret := _m.Called(message, opts)
if len(ret) == 0 {
panic("no return value specified for Commit")
}
var r0 plumbing.Hash
var r1 error
if rf, ok := ret.Get(0).(func(string, *git.CommitOptions) (plumbing.Hash, error)); ok {
return rf(message, opts)
}
if rf, ok := ret.Get(0).(func(string, *git.CommitOptions) plumbing.Hash); ok {
r0 = rf(message, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(plumbing.Hash)
}
}
if rf, ok := ret.Get(1).(func(string, *git.CommitOptions) error); ok {
r1 = rf(message, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockWorktree_Commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commit'
type MockWorktree_Commit_Call struct {
*mock.Call
}
// Commit is a helper method to define mock.On call
// - message string
// - opts *git.CommitOptions
func (_e *MockWorktree_Expecter) Commit(message interface{}, opts interface{}) *MockWorktree_Commit_Call {
return &MockWorktree_Commit_Call{Call: _e.mock.On("Commit", message, opts)}
}
func (_c *MockWorktree_Commit_Call) Run(run func(message string, opts *git.CommitOptions)) *MockWorktree_Commit_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(*git.CommitOptions))
})
return _c
}
func (_c *MockWorktree_Commit_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Commit_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockWorktree_Commit_Call) RunAndReturn(run func(string, *git.CommitOptions) (plumbing.Hash, error)) *MockWorktree_Commit_Call {
_c.Call.Return(run)
return _c
}
// Filesystem provides a mock function with no fields
func (_m *MockWorktree) Filesystem() billy.Filesystem {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Filesystem")
}
var r0 billy.Filesystem
if rf, ok := ret.Get(0).(func() billy.Filesystem); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(billy.Filesystem)
}
}
return r0
}
// MockWorktree_Filesystem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Filesystem'
type MockWorktree_Filesystem_Call struct {
*mock.Call
}
// Filesystem is a helper method to define mock.On call
func (_e *MockWorktree_Expecter) Filesystem() *MockWorktree_Filesystem_Call {
return &MockWorktree_Filesystem_Call{Call: _e.mock.On("Filesystem")}
}
func (_c *MockWorktree_Filesystem_Call) Run(run func()) *MockWorktree_Filesystem_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockWorktree_Filesystem_Call) Return(_a0 billy.Filesystem) *MockWorktree_Filesystem_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWorktree_Filesystem_Call) RunAndReturn(run func() billy.Filesystem) *MockWorktree_Filesystem_Call {
_c.Call.Return(run)
return _c
}
// Remove provides a mock function with given fields: path
func (_m *MockWorktree) Remove(path string) (plumbing.Hash, error) {
ret := _m.Called(path)
if len(ret) == 0 {
panic("no return value specified for Remove")
}
var r0 plumbing.Hash
var r1 error
if rf, ok := ret.Get(0).(func(string) (plumbing.Hash, error)); ok {
return rf(path)
}
if rf, ok := ret.Get(0).(func(string) plumbing.Hash); ok {
r0 = rf(path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(plumbing.Hash)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockWorktree_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
type MockWorktree_Remove_Call struct {
*mock.Call
}
// Remove is a helper method to define mock.On call
// - path string
func (_e *MockWorktree_Expecter) Remove(path interface{}) *MockWorktree_Remove_Call {
return &MockWorktree_Remove_Call{Call: _e.mock.On("Remove", path)}
}
func (_c *MockWorktree_Remove_Call) Run(run func(path string)) *MockWorktree_Remove_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockWorktree_Remove_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Remove_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockWorktree_Remove_Call) RunAndReturn(run func(string) (plumbing.Hash, error)) *MockWorktree_Remove_Call {
_c.Call.Return(run)
return _c
}
// NewMockWorktree creates a new instance of MockWorktree. 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 NewMockWorktree(t interface {
mock.TestingT
Cleanup(func())
}) *MockWorktree {
mock := &MockWorktree{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,468 +0,0 @@
package gogit
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"strings"
"time"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/util"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/client"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
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/grafana/pkg/util/httpclient"
)
const (
// maxOperationBytes is the maximum size of a git operation in bytes (1 GB)
maxOperationBytes = int64(1 << 30)
maxOperationTimeout = 10 * time.Minute
)
func init() {
// Create a size-limited writer that will cancel the context if size is exceeded
limitedTransport := NewByteLimitedTransport(httpclient.NewHTTPTransport(), maxOperationBytes)
httpClient := githttp.NewClient(&http.Client{
Transport: limitedTransport,
})
client.InstallProtocol("https", httpClient)
client.InstallProtocol("http", httpClient)
}
//go:generate mockery --name=Worktree --output=mocks --inpackage --filename=worktree_mock.go --with-expecter
type Worktree interface {
Commit(message string, opts *git.CommitOptions) (plumbing.Hash, error)
Remove(path string) (plumbing.Hash, error)
Add(path string) (plumbing.Hash, error)
Filesystem() billy.Filesystem
}
type worktree struct {
*git.Worktree
}
//go:generate mockery --name=Repository --output=mocks --inpackage --filename=repository_mock.go --with-expecter
type Repository interface {
PushContext(ctx context.Context, o *git.PushOptions) error
}
func (w *worktree) Filesystem() billy.Filesystem {
return w.Worktree.Filesystem
}
var _ repository.Repository = (*GoGitRepo)(nil)
type GoGitRepo struct {
config *provisioning.Repository
decryptedPassword string
opts repository.CloneOptions
repo Repository
tree Worktree
dir string // file path to worktree root (necessary? should use billy)
}
// This will create a new clone every time
// As structured, it is valid for one context and should not be shared across multiple requests
func Clone(
ctx context.Context,
root string,
config *provisioning.Repository,
opts repository.CloneOptions,
secrets secrets.Service,
) (repository.ClonedRepository, error) {
if root == "" {
return nil, fmt.Errorf("missing root config")
}
if config.Namespace == "" {
return nil, fmt.Errorf("config is missing namespace")
}
if config.Name == "" {
return nil, fmt.Errorf("config is missing name")
}
if opts.BeforeFn != nil {
if err := opts.BeforeFn(); err != nil {
return nil, err
}
}
// add a timeout to the operation
timeout := maxOperationTimeout
if opts.Timeout > 0 {
timeout = opts.Timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken)
if err != nil {
return nil, fmt.Errorf("error decrypting token: %w", err)
}
if err := os.MkdirAll(root, 0700); err != nil {
return nil, fmt.Errorf("create root dir: %w", err)
}
dir, err := os.MkdirTemp(root, fmt.Sprintf("clone-%s-%s-", config.Namespace, config.Name))
if err != nil {
return nil, fmt.Errorf("create temp clone dir: %w", err)
}
progress := opts.Progress
if progress == nil {
progress = io.Discard
}
repo, tree, err := clone(ctx, config, opts, decrypted, dir, progress)
if err != nil {
if err := os.RemoveAll(dir); err != nil {
return nil, fmt.Errorf("remove temp clone dir after clone failed: %w", err)
}
return nil, fmt.Errorf("clone: %w", err)
}
return &GoGitRepo{
config: config,
tree: &worktree{Worktree: tree},
opts: opts,
decryptedPassword: string(decrypted),
repo: repo,
dir: dir,
}, nil
}
func clone(ctx context.Context, config *provisioning.Repository, opts repository.CloneOptions, decrypted []byte, dir string, progress io.Writer) (*git.Repository, *git.Worktree, error) {
gitcfg := config.Spec.GitHub
url := gitcfg.URL
if !strings.HasPrefix(url, "file://") {
url = fmt.Sprintf("%s.git", url)
}
branch := plumbing.NewBranchReferenceName(gitcfg.Branch)
cloneOpts := &git.CloneOptions{
ReferenceName: branch,
Auth: &githttp.BasicAuth{
Username: "grafana", // this can be anything except an empty string for PAT
Password: string(decrypted), // TODO... will need to get from a service!
},
URL: url,
Progress: progress,
}
repo, err := git.PlainCloneContext(ctx, dir, false, cloneOpts)
if errors.Is(err, plumbing.ErrReferenceNotFound) && opts.CreateIfNotExists {
cloneOpts.ReferenceName = "" // empty
repo, err = git.PlainCloneContext(ctx, dir, false, cloneOpts)
if err == nil {
worktree, err := repo.Worktree()
if err != nil {
return nil, nil, err
}
err = worktree.Checkout(&git.CheckoutOptions{
Branch: branch,
Force: true,
Create: true,
})
if err != nil {
return nil, nil, fmt.Errorf("unable to create new branch: %w", err)
}
}
} else if err != nil {
return nil, nil, fmt.Errorf("clone error: %w", err)
}
rcfg, err := repo.Config()
if err != nil {
return nil, nil, fmt.Errorf("error reading repository config %w", err)
}
origin := rcfg.Remotes["origin"]
if origin == nil {
return nil, nil, fmt.Errorf("missing origin remote %w", err)
}
if url != origin.URLs[0] {
return nil, nil, fmt.Errorf("unexpected remote (expected: %s, found: %s)", url, origin.URLs[0])
}
worktree, err := repo.Worktree()
if err != nil {
return nil, nil, fmt.Errorf("get worktree: %w", err)
}
return repo, worktree, nil
}
// After making changes to the worktree, push changes
func (g *GoGitRepo) Push(ctx context.Context, opts repository.PushOptions) error {
timeout := maxOperationTimeout
if opts.Timeout > 0 {
timeout = opts.Timeout
}
progress := opts.Progress
if progress == nil {
progress = io.Discard
}
if opts.BeforeFn != nil {
if err := opts.BeforeFn(); err != nil {
return err
}
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
if !g.opts.PushOnWrites {
_, err := g.tree.Commit("exported from grafana", &git.CommitOptions{
All: true, // Add everything that changed
})
if err != nil {
// empty commit is fine -- no change
if !errors.Is(err, git.ErrEmptyCommit) {
return err
}
}
}
err := g.repo.PushContext(ctx, &git.PushOptions{
Progress: progress,
Force: true, // avoid fast-forward-errors
Auth: &githttp.BasicAuth{ // reuse logic from clone?
Username: "grafana",
Password: g.decryptedPassword,
},
})
if errors.Is(err, git.NoErrAlreadyUpToDate) {
return nil // same as the target
}
return err
}
func (g *GoGitRepo) Remove(ctx context.Context) error {
return os.RemoveAll(g.dir)
}
// Config implements repository.Repository.
func (g *GoGitRepo) Config() *provisioning.Repository {
return g.config
}
// ReadTree implements repository.Repository.
func (g *GoGitRepo) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) {
var treePath string
if g.config.Spec.GitHub.Path != "" {
treePath = g.config.Spec.GitHub.Path
}
treePath = safepath.Clean(treePath)
entries := make([]repository.FileTreeEntry, 0, 100)
err := util.Walk(g.tree.Filesystem(), treePath, func(path string, info fs.FileInfo, err error) error {
// We already have an error, just pass it onwards.
if err != nil ||
// This is the root of the repository (or should pretend to be)
safepath.Clean(path) == "" || path == treePath ||
// This is the Git data
(treePath == "" && (strings.HasPrefix(path, ".git/") || path == ".git")) {
return err
}
if treePath != "" {
path = strings.TrimPrefix(path, treePath)
}
entry := repository.FileTreeEntry{
Path: strings.TrimLeft(path, "/"),
Size: info.Size(),
}
if !info.IsDir() {
entry.Blob = true
// For a real instance, this will likely be based on:
// https://github.com/go-git/go-git/blob/main/_examples/ls/main.go#L25
entry.Hash = fmt.Sprintf("TODO/%d", info.Size()) // but not used for
}
entries = append(entries, entry)
return err
})
if errors.Is(err, fs.ErrNotExist) {
// We intentionally ignore this case, as it is expected
} else if err != nil {
return nil, fmt.Errorf("walk tree for ref '%s': %w", ref, err)
}
return entries, nil
}
func (g *GoGitRepo) Test(ctx context.Context) (*provisioning.TestResults, error) {
return &provisioning.TestResults{
Success: g.tree != nil,
}, nil
}
// Update implements repository.Repository.
func (g *GoGitRepo) Update(ctx context.Context, path string, ref string, data []byte, message string) error {
return g.Write(ctx, path, ref, data, message)
}
// Create implements repository.Repository.
func (g *GoGitRepo) Create(ctx context.Context, path string, ref string, data []byte, message string) error {
// FIXME: this means we would override files
return g.Write(ctx, path, ref, data, message)
}
// Write implements repository.Repository.
func (g *GoGitRepo) Write(ctx context.Context, fpath string, ref string, data []byte, message string) error {
if err := verifyPathWithoutRef(fpath, ref); err != nil {
return err
}
fpath = safepath.Join(g.config.Spec.GitHub.Path, fpath)
// FIXME: this means that won't export empty folders
// should we create them with a .keep file?
// For folders, just create the folder and ignore the commit
if safepath.IsDir(fpath) {
return g.tree.Filesystem().MkdirAll(fpath, 0750)
}
dir := safepath.Dir(fpath)
if dir != "" {
err := g.tree.Filesystem().MkdirAll(dir, 0750)
if err != nil {
return err
}
}
file, err := g.tree.Filesystem().Create(fpath)
if err != nil {
return err
}
_, err = file.Write(data)
if err != nil {
return err
}
_, err = g.tree.Add(fpath)
if err != nil {
return err
}
return g.maybeCommit(ctx, message)
}
func (g *GoGitRepo) maybeCommit(ctx context.Context, message string) error {
// Skip commit for each file
if !g.opts.PushOnWrites {
return nil
}
opts := &git.CommitOptions{
Author: &object.Signature{
Name: "grafana",
},
}
sig := repository.GetAuthorSignature(ctx)
if sig != nil && sig.Name != "" {
opts.Author.Name = sig.Name
opts.Author.Email = sig.Email
opts.Author.When = sig.When
}
if opts.Author.When.IsZero() {
opts.Author.When = time.Now()
}
_, err := g.tree.Commit(message, opts)
if errors.Is(err, git.ErrEmptyCommit) {
return nil // empty commit is fine -- no change
}
return err
}
// Delete implements repository.Repository.
func (g *GoGitRepo) Delete(ctx context.Context, fpath string, ref string, message string) error {
if err := verifyPathWithoutRef(fpath, ref); err != nil {
return err
}
fpath = safepath.Join(g.config.Spec.GitHub.Path, fpath)
if _, err := g.tree.Remove(fpath); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return repository.ErrFileNotFound
}
return err
}
return g.maybeCommit(ctx, message)
}
// Read implements repository.Repository.
func (g *GoGitRepo) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) {
if err := verifyPathWithoutRef(path, ref); err != nil {
return nil, err
}
readPath := safepath.Join(g.config.Spec.GitHub.Path, path)
stat, err := g.tree.Filesystem().Lstat(readPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, repository.ErrFileNotFound
} else if err != nil {
return nil, fmt.Errorf("stat path '%s': %w", readPath, err)
}
info := &repository.FileInfo{
Path: path,
Modified: &metav1.Time{
Time: stat.ModTime(),
},
}
if !stat.IsDir() {
f, err := g.tree.Filesystem().Open(readPath)
if err != nil {
return nil, fmt.Errorf("open file '%s': %w", readPath, err)
}
info.Data, err = io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read file '%s': %w", readPath, err)
}
}
return info, err
}
func verifyPathWithoutRef(path string, ref string) error {
if path == "" {
return fmt.Errorf("expected path")
}
if ref != "" {
return fmt.Errorf("ref unsupported")
}
return nil
}
// History implements repository.Repository.
func (g *GoGitRepo) History(ctx context.Context, path string, ref string) ([]provisioning.HistoryItem, error) {
return nil, &apierrors.StatusError{
ErrStatus: metav1.Status{
Message: "history is not yet implemented",
Code: http.StatusNotImplemented,
},
}
}
// Validate implements repository.Repository.
func (g *GoGitRepo) Validate() field.ErrorList {
return nil
}

View File

@@ -1,4 +1,4 @@
package repository
package local
import (
"context"
@@ -23,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
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"
)
@@ -75,9 +76,9 @@ func (r *LocalFolderResolver) LocalPath(p string) (string, error) {
}
var (
_ Repository = (*localRepository)(nil)
_ Writer = (*localRepository)(nil)
_ Reader = (*localRepository)(nil)
_ repository.Repository = (*localRepository)(nil)
_ repository.Writer = (*localRepository)(nil)
_ repository.Reader = (*localRepository)(nil)
)
type localRepository struct {
@@ -147,17 +148,17 @@ func (r *localRepository) Validate() field.ErrorList {
func (r *localRepository) Test(ctx context.Context) (*provisioning.TestResults, error) {
path := field.NewPath("spec", "local", "path")
if r.config.Spec.Local.Path == "" {
return fromFieldError(field.Required(path, "no path is configured")), nil
return repository.FromFieldError(field.Required(path, "no path is configured")), nil
}
_, err := r.resolver.LocalPath(r.config.Spec.Local.Path)
if err != nil {
return fromFieldError(field.Invalid(path, r.config.Spec.Local.Path, err.Error())), nil
return repository.FromFieldError(field.Invalid(path, r.config.Spec.Local.Path, err.Error())), nil
}
_, err = os.Stat(r.path)
if errors.Is(err, os.ErrNotExist) {
return fromFieldError(field.NotFound(path, r.config.Spec.Local.Path)), nil
return repository.FromFieldError(field.NotFound(path, r.config.Spec.Local.Path)), nil
}
return &provisioning.TestResults{
@@ -176,7 +177,7 @@ func (r *localRepository) validateRequest(ref string) error {
}
// ReadResource implements provisioning.Repository.
func (r *localRepository) Read(ctx context.Context, filePath string, ref string) (*FileInfo, error) {
func (r *localRepository) Read(ctx context.Context, filePath string, ref string) (*repository.FileInfo, error) {
if err := r.validateRequest(ref); err != nil {
return nil, err
}
@@ -184,13 +185,13 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string)
actualPath := safepath.Join(r.path, filePath)
info, err := os.Stat(actualPath)
if errors.Is(err, os.ErrNotExist) {
return nil, ErrFileNotFound
return nil, repository.ErrFileNotFound
} else if err != nil {
return nil, fmt.Errorf("stat file: %w", err)
}
if info.IsDir() {
return &FileInfo{
return &repository.FileInfo{
Path: filePath,
Modified: &metav1.Time{
Time: info.ModTime(),
@@ -209,7 +210,7 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string)
return nil, fmt.Errorf("calculate hash of file: %w", err)
}
return &FileInfo{
return &repository.FileInfo{
Path: filePath,
Data: data,
Hash: hash,
@@ -220,7 +221,7 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string)
}
// ReadResource implements provisioning.Repository.
func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) {
func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) {
if err := r.validateRequest(ref); err != nil {
return nil, err
}
@@ -228,16 +229,16 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE
// Return an empty list when folder does not exist
_, err := os.Stat(r.path)
if errors.Is(err, fs.ErrNotExist) {
return []FileTreeEntry{}, nil
return []repository.FileTreeEntry{}, nil
}
rootlen := len(r.path)
entries := make([]FileTreeEntry, 0, 100)
entries := make([]repository.FileTreeEntry, 0, 100)
err = filepath.Walk(r.path, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
entry := FileTreeEntry{
entry := repository.FileTreeEntry{
Path: strings.TrimLeft(path[rootlen:], "/"),
Size: info.Size(),
}
@@ -251,8 +252,10 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE
if err != nil {
return fmt.Errorf("read and calculate hash of path %s: %w", path, err)
}
} else if !strings.HasSuffix(entry.Path, "/") {
// ensure trailing slash for directories
entry.Path = entry.Path + "/"
}
// TODO: do folders have a trailing slash?
entries = append(entries, entry)
return err
})
@@ -263,7 +266,7 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE
func (r *localRepository) calculateFileHash(path string) (string, int64, error) {
// Treats https://securego.io/docs/rules/g304.html
if !safepath.InDir(path, r.path) {
return "", 0, ErrFileNotFound
return "", 0, repository.ErrFileNotFound
}
// We've already made sure the path is safe, so we'll ignore the gosec lint.
@@ -329,7 +332,7 @@ func (r *localRepository) Update(ctx context.Context, path string, ref string, d
f, err := os.Stat(path)
if err != nil && errors.Is(err, os.ErrNotExist) {
return ErrFileNotFound
return repository.ErrFileNotFound
}
if f.IsDir() {
return apierrors.NewBadRequest("path exists but it is a directory")
@@ -360,5 +363,12 @@ func (r *localRepository) Delete(ctx context.Context, path string, ref string, c
return err
}
return os.Remove(safepath.Join(r.path, path))
fullPath := safepath.Join(r.path, path)
if safepath.IsDir(path) {
// if it is a folder, delete all of its contents
return os.RemoveAll(fullPath)
}
return os.Remove(fullPath)
}

View File

@@ -1,4 +1,4 @@
package repository
package local
import (
"context"
@@ -19,6 +19,7 @@ import (
field "k8s.io/apimachinery/pkg/util/validation/field"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
)
func TestLocalResolver(t *testing.T) {
@@ -91,14 +92,14 @@ func TestLocalResolver(t *testing.T) {
// Verify all directories and files are present
expectedPaths := []string{
"another",
"another/path",
"another/",
"another/path/",
"another/path/file.txt",
"level1",
"level1/",
"level1/file1.txt",
"level1/level2",
"level1/level2/",
"level1/level2/file2.txt",
"level1/level2/level3",
"level1/level2/level3/",
"level1/level2/level3/file3.txt",
"root.txt",
}
@@ -542,6 +543,41 @@ func TestLocalRepository_Delete(t *testing.T) {
comment: "test delete with ref",
expectedErr: apierrors.NewBadRequest("local repository does not support ref"),
},
{
name: "delete folder with nested files",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
nestedFolderPath := filepath.Join(tempDir, "folder")
err := os.MkdirAll(nestedFolderPath, 0700)
require.NoError(t, err)
subFolderPath := filepath.Join(nestedFolderPath, "nested-folder")
err = os.MkdirAll(subFolderPath, 0700)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(nestedFolderPath, "nested-dash.txt"), []byte("content1"), 0600)
require.NoError(t, err)
// Create repository with the temp directory as permitted prefix
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "folder/",
ref: "",
comment: "test delete folder with nested content",
expectedErr: nil,
},
}
for _, tc := range testCases {
@@ -668,7 +704,7 @@ func TestLocalRepository_Update(t *testing.T) {
ref: "",
data: []byte("content"),
comment: "",
expectedErr: ErrFileNotFound,
expectedErr: repository.ErrFileNotFound,
},
{
name: "update directory",
@@ -1139,7 +1175,7 @@ func TestLocalRepository_Read(t *testing.T) {
path string
ref string
expectedErr error
expected *FileInfo
expected *repository.FileInfo
}{
{
name: "read existing file",
@@ -1168,7 +1204,7 @@ func TestLocalRepository_Read(t *testing.T) {
return tempDir, repo
},
path: "test-file.txt",
expected: &FileInfo{
expected: &repository.FileInfo{
Path: "test-file.txt",
Modified: &metav1.Time{Time: time.Now()},
Data: []byte("test content"),
@@ -1196,7 +1232,7 @@ func TestLocalRepository_Read(t *testing.T) {
return tempDir, repo
},
path: "non-existent-file.txt",
expectedErr: ErrFileNotFound,
expectedErr: repository.ErrFileNotFound,
},
{
name: "read with ref should fail",
@@ -1254,7 +1290,7 @@ func TestLocalRepository_Read(t *testing.T) {
return tempDir, repo
},
path: "test-dir",
expected: &FileInfo{
expected: &repository.FileInfo{
Path: "test-dir",
Modified: &metav1.Time{Time: time.Now()},
},
@@ -1292,7 +1328,7 @@ func TestLocalRepository_ReadTree(t *testing.T) {
setup func(t *testing.T) (string, *localRepository)
ref string
expectedErr error
expected []FileTreeEntry
expected []repository.FileTreeEntry
}{
{
name: "read empty directory",
@@ -1314,7 +1350,7 @@ func TestLocalRepository_ReadTree(t *testing.T) {
return tempDir, repo
},
expected: []FileTreeEntry{},
expected: []repository.FileTreeEntry{},
expectedErr: nil,
},
{
@@ -1344,10 +1380,10 @@ func TestLocalRepository_ReadTree(t *testing.T) {
return tempDir, repo
},
expected: []FileTreeEntry{
expected: []repository.FileTreeEntry{
{Path: "file1.txt", Blob: true, Size: 8},
{Path: "file2.txt", Blob: true, Size: 8},
{Path: "subdir", Blob: false},
{Path: "subdir/", Blob: false},
{Path: "subdir/file3.txt", Blob: true, Size: 8},
},
expectedErr: nil,
@@ -1397,7 +1433,7 @@ func TestLocalRepository_ReadTree(t *testing.T) {
return tempDir, repo
},
expected: []FileTreeEntry{},
expected: []repository.FileTreeEntry{},
expectedErr: nil,
},
}

View File

@@ -1,125 +0,0 @@
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)
}

View File

@@ -1,356 +0,0 @@
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)
})
}
}

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository

View File

@@ -2,9 +2,7 @@ package repository
import (
"context"
"io"
"net/http"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -67,47 +65,6 @@ type FileInfo struct {
Modified *metav1.Time
}
//go:generate mockery --name CloneFn --structname MockCloneFn --inpackage --filename clone_fn_mock.go --with-expecter
type CloneFn func(ctx context.Context, opts CloneOptions) (ClonedRepository, error)
type CloneOptions struct {
// If the branch does not exist, create it
CreateIfNotExists bool
// Push on every write
PushOnWrites bool
// Maximum allowed size for repository clone in bytes (0 means no limit)
MaxSize int64
// Maximum time allowed for clone operation in seconds (0 means no limit)
Timeout time.Duration
// Progress is the writer to report progress to
Progress io.Writer
// BeforeFn is called before the clone operation starts
BeforeFn func() error
}
//go:generate mockery --name ClonableRepository --structname MockClonableRepository --inpackage --filename clonable_repository_mock.go --with-expecter
type ClonableRepository interface {
Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error)
}
type PushOptions struct {
Timeout time.Duration
Progress io.Writer
BeforeFn func() error
}
//go:generate mockery --name ClonedRepository --structname MockClonedRepository --inpackage --filename cloned_repository_mock.go --with-expecter
type ClonedRepository interface {
ReaderWriter
Push(ctx context.Context, opts PushOptions) error
Remove(ctx context.Context) error
}
// An entry in the file tree, as returned by 'ReadFileTree'. Like FileInfo, but contains less information.
type FileTreeEntry struct {
// The path to the file from the base path given (if any).

View File

@@ -0,0 +1,95 @@
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// MockStageableRepository is an autogenerated mock type for the StageableRepository type
type MockStageableRepository struct {
mock.Mock
}
type MockStageableRepository_Expecter struct {
mock *mock.Mock
}
func (_m *MockStageableRepository) EXPECT() *MockStageableRepository_Expecter {
return &MockStageableRepository_Expecter{mock: &_m.Mock}
}
// Stage provides a mock function with given fields: ctx, opts
func (_m *MockStageableRepository) Stage(ctx context.Context, opts StageOptions) (StagedRepository, error) {
ret := _m.Called(ctx, opts)
if len(ret) == 0 {
panic("no return value specified for Stage")
}
var r0 StagedRepository
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, StageOptions) (StagedRepository, error)); ok {
return rf(ctx, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, StageOptions) StagedRepository); ok {
r0 = rf(ctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(StagedRepository)
}
}
if rf, ok := ret.Get(1).(func(context.Context, StageOptions) error); ok {
r1 = rf(ctx, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockStageableRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage'
type MockStageableRepository_Stage_Call struct {
*mock.Call
}
// Stage is a helper method to define mock.On call
// - ctx context.Context
// - opts StageOptions
func (_e *MockStageableRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockStageableRepository_Stage_Call {
return &MockStageableRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)}
}
func (_c *MockStageableRepository_Stage_Call) Run(run func(ctx context.Context, opts StageOptions)) *MockStageableRepository_Stage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(StageOptions))
})
return _c
}
func (_c *MockStageableRepository_Stage_Call) Return(_a0 StagedRepository, _a1 error) *MockStageableRepository_Stage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockStageableRepository_Stage_Call) RunAndReturn(run func(context.Context, StageOptions) (StagedRepository, error)) *MockStageableRepository_Stage_Call {
_c.Call.Return(run)
return _c
}
// NewMockStageableRepository creates a new instance of MockStageableRepository. 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 NewMockStageableRepository(t interface {
mock.TestingT
Cleanup(func())
}) *MockStageableRepository {
mock := &MockStageableRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -0,0 +1,71 @@
package repository
import (
context "context"
"errors"
"fmt"
"time"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/nanogit"
)
type StageOptions struct {
// Push on every write
PushOnWrites bool
// Maximum time allowed for clone operation in seconds (0 means no limit)
Timeout time.Duration
}
//go:generate mockery --name StageableRepository --structname MockStageableRepository --inpackage --filename stageable_repository_mock.go --with-expecter
type StageableRepository interface {
Stage(ctx context.Context, opts StageOptions) (StagedRepository, error)
}
//go:generate mockery --name StagedRepository --structname MockStagedRepository --inpackage --filename staged_repository_mock.go --with-expecter
type StagedRepository interface {
ReaderWriter
Push(ctx context.Context) error
Remove(ctx context.Context) error
}
// WrapWithStageAndPushIfPossible attempts to stage the given repository. If staging is supported,
// it runs the provided function on the staged repository, then pushes any changes and cleans up the staged repository.
// If staging is not supported, it runs the function on the original repository without pushing.
// The 'staged' argument to the function indicates whether a staged repository was used.
func WrapWithStageAndPushIfPossible(
ctx context.Context,
repo Repository,
stageOptions StageOptions,
fn func(repo Repository, staged bool) error,
) error {
stageable, ok := repo.(StageableRepository)
if !ok {
return fn(repo, false)
}
staged, err := stageable.Stage(ctx, stageOptions)
if err != nil {
return fmt.Errorf("stage repository: %w", err)
}
// We don't, we simply log it
// FIXME: should we handle this differently?
defer func() {
if err := staged.Remove(ctx); err != nil {
logging.FromContext(ctx).Error("failed to remove staged repository after export", "err", err)
}
}()
if err := fn(staged, true); err != nil {
return err
}
if err = staged.Push(ctx); err != nil {
if errors.Is(err, nanogit.ErrNothingToPush) {
return nil // OK, already pushed
}
return fmt.Errorf("wrapped push error: %w", err)
}
return nil
}

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository
@@ -11,21 +11,21 @@ import (
v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
// MockClonedRepository is an autogenerated mock type for the ClonedRepository type
type MockClonedRepository struct {
// MockStagedRepository is an autogenerated mock type for the StagedRepository type
type MockStagedRepository struct {
mock.Mock
}
type MockClonedRepository_Expecter struct {
type MockStagedRepository_Expecter struct {
mock *mock.Mock
}
func (_m *MockClonedRepository) EXPECT() *MockClonedRepository_Expecter {
return &MockClonedRepository_Expecter{mock: &_m.Mock}
func (_m *MockStagedRepository) EXPECT() *MockStagedRepository_Expecter {
return &MockStagedRepository_Expecter{mock: &_m.Mock}
}
// Config provides a mock function with no fields
func (_m *MockClonedRepository) Config() *v0alpha1.Repository {
func (_m *MockStagedRepository) Config() *v0alpha1.Repository {
ret := _m.Called()
if len(ret) == 0 {
@@ -44,35 +44,35 @@ func (_m *MockClonedRepository) Config() *v0alpha1.Repository {
return r0
}
// MockClonedRepository_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config'
type MockClonedRepository_Config_Call struct {
// MockStagedRepository_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config'
type MockStagedRepository_Config_Call struct {
*mock.Call
}
// Config is a helper method to define mock.On call
func (_e *MockClonedRepository_Expecter) Config() *MockClonedRepository_Config_Call {
return &MockClonedRepository_Config_Call{Call: _e.mock.On("Config")}
func (_e *MockStagedRepository_Expecter) Config() *MockStagedRepository_Config_Call {
return &MockStagedRepository_Config_Call{Call: _e.mock.On("Config")}
}
func (_c *MockClonedRepository_Config_Call) Run(run func()) *MockClonedRepository_Config_Call {
func (_c *MockStagedRepository_Config_Call) Run(run func()) *MockStagedRepository_Config_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockClonedRepository_Config_Call) Return(_a0 *v0alpha1.Repository) *MockClonedRepository_Config_Call {
func (_c *MockStagedRepository_Config_Call) Return(_a0 *v0alpha1.Repository) *MockStagedRepository_Config_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClonedRepository_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockClonedRepository_Config_Call {
func (_c *MockStagedRepository_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockStagedRepository_Config_Call {
_c.Call.Return(run)
return _c
}
// Create provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockClonedRepository) Create(ctx context.Context, path string, ref string, data []byte, message string) error {
func (_m *MockStagedRepository) 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 {
@@ -89,8 +89,8 @@ func (_m *MockClonedRepository) Create(ctx context.Context, path string, ref str
return r0
}
// MockClonedRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
type MockClonedRepository_Create_Call struct {
// MockStagedRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
type MockStagedRepository_Create_Call struct {
*mock.Call
}
@@ -100,29 +100,29 @@ type MockClonedRepository_Create_Call struct {
// - ref string
// - data []byte
// - message string
func (_e *MockClonedRepository_Expecter) Create(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Create_Call {
return &MockClonedRepository_Create_Call{Call: _e.mock.On("Create", ctx, path, ref, data, message)}
func (_e *MockStagedRepository_Expecter) Create(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Create_Call {
return &MockStagedRepository_Create_Call{Call: _e.mock.On("Create", ctx, path, ref, data, message)}
}
func (_c *MockClonedRepository_Create_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Create_Call {
func (_c *MockStagedRepository_Create_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_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 *MockClonedRepository_Create_Call) Return(_a0 error) *MockClonedRepository_Create_Call {
func (_c *MockStagedRepository_Create_Call) Return(_a0 error) *MockStagedRepository_Create_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClonedRepository_Create_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Create_Call {
func (_c *MockStagedRepository_Create_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Create_Call {
_c.Call.Return(run)
return _c
}
// Delete provides a mock function with given fields: ctx, path, ref, message
func (_m *MockClonedRepository) Delete(ctx context.Context, path string, ref string, message string) error {
func (_m *MockStagedRepository) Delete(ctx context.Context, path string, ref string, message string) error {
ret := _m.Called(ctx, path, ref, message)
if len(ret) == 0 {
@@ -139,8 +139,8 @@ func (_m *MockClonedRepository) Delete(ctx context.Context, path string, ref str
return r0
}
// MockClonedRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type MockClonedRepository_Delete_Call struct {
// MockStagedRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type MockStagedRepository_Delete_Call struct {
*mock.Call
}
@@ -149,38 +149,38 @@ type MockClonedRepository_Delete_Call struct {
// - path string
// - ref string
// - message string
func (_e *MockClonedRepository_Expecter) Delete(ctx interface{}, path interface{}, ref interface{}, message interface{}) *MockClonedRepository_Delete_Call {
return &MockClonedRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, path, ref, message)}
func (_e *MockStagedRepository_Expecter) Delete(ctx interface{}, path interface{}, ref interface{}, message interface{}) *MockStagedRepository_Delete_Call {
return &MockStagedRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, path, ref, message)}
}
func (_c *MockClonedRepository_Delete_Call) Run(run func(ctx context.Context, path string, ref string, message string)) *MockClonedRepository_Delete_Call {
func (_c *MockStagedRepository_Delete_Call) Run(run func(ctx context.Context, path string, ref string, message string)) *MockStagedRepository_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 *MockClonedRepository_Delete_Call) Return(_a0 error) *MockClonedRepository_Delete_Call {
func (_c *MockStagedRepository_Delete_Call) Return(_a0 error) *MockStagedRepository_Delete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClonedRepository_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockClonedRepository_Delete_Call {
func (_c *MockStagedRepository_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockStagedRepository_Delete_Call {
_c.Call.Return(run)
return _c
}
// Push provides a mock function with given fields: ctx, opts
func (_m *MockClonedRepository) Push(ctx context.Context, opts PushOptions) error {
ret := _m.Called(ctx, opts)
// Push provides a mock function with given fields: ctx
func (_m *MockStagedRepository) Push(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Push")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, PushOptions) error); ok {
r0 = rf(ctx, opts)
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
@@ -188,37 +188,36 @@ func (_m *MockClonedRepository) Push(ctx context.Context, opts PushOptions) erro
return r0
}
// MockClonedRepository_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push'
type MockClonedRepository_Push_Call struct {
// MockStagedRepository_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push'
type MockStagedRepository_Push_Call struct {
*mock.Call
}
// Push is a helper method to define mock.On call
// - ctx context.Context
// - opts PushOptions
func (_e *MockClonedRepository_Expecter) Push(ctx interface{}, opts interface{}) *MockClonedRepository_Push_Call {
return &MockClonedRepository_Push_Call{Call: _e.mock.On("Push", ctx, opts)}
func (_e *MockStagedRepository_Expecter) Push(ctx interface{}) *MockStagedRepository_Push_Call {
return &MockStagedRepository_Push_Call{Call: _e.mock.On("Push", ctx)}
}
func (_c *MockClonedRepository_Push_Call) Run(run func(ctx context.Context, opts PushOptions)) *MockClonedRepository_Push_Call {
func (_c *MockStagedRepository_Push_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Push_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(PushOptions))
run(args[0].(context.Context))
})
return _c
}
func (_c *MockClonedRepository_Push_Call) Return(_a0 error) *MockClonedRepository_Push_Call {
func (_c *MockStagedRepository_Push_Call) Return(_a0 error) *MockStagedRepository_Push_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClonedRepository_Push_Call) RunAndReturn(run func(context.Context, PushOptions) error) *MockClonedRepository_Push_Call {
func (_c *MockStagedRepository_Push_Call) RunAndReturn(run func(context.Context) error) *MockStagedRepository_Push_Call {
_c.Call.Return(run)
return _c
}
// Read provides a mock function with given fields: ctx, path, ref
func (_m *MockClonedRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) {
func (_m *MockStagedRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) {
ret := _m.Called(ctx, path, ref)
if len(ret) == 0 {
@@ -247,8 +246,8 @@ func (_m *MockClonedRepository) Read(ctx context.Context, path string, ref strin
return r0, r1
}
// MockClonedRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
type MockClonedRepository_Read_Call struct {
// MockStagedRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
type MockStagedRepository_Read_Call struct {
*mock.Call
}
@@ -256,29 +255,29 @@ type MockClonedRepository_Read_Call struct {
// - ctx context.Context
// - path string
// - ref string
func (_e *MockClonedRepository_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockClonedRepository_Read_Call {
return &MockClonedRepository_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)}
func (_e *MockStagedRepository_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockStagedRepository_Read_Call {
return &MockStagedRepository_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)}
}
func (_c *MockClonedRepository_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockClonedRepository_Read_Call {
func (_c *MockStagedRepository_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockStagedRepository_Read_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockClonedRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockClonedRepository_Read_Call {
func (_c *MockStagedRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockStagedRepository_Read_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClonedRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockClonedRepository_Read_Call {
func (_c *MockStagedRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockStagedRepository_Read_Call {
_c.Call.Return(run)
return _c
}
// ReadTree provides a mock function with given fields: ctx, ref
func (_m *MockClonedRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) {
func (_m *MockStagedRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) {
ret := _m.Called(ctx, ref)
if len(ret) == 0 {
@@ -307,37 +306,37 @@ func (_m *MockClonedRepository) ReadTree(ctx context.Context, ref string) ([]Fil
return r0, r1
}
// MockClonedRepository_ReadTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadTree'
type MockClonedRepository_ReadTree_Call struct {
// MockStagedRepository_ReadTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadTree'
type MockStagedRepository_ReadTree_Call struct {
*mock.Call
}
// ReadTree is a helper method to define mock.On call
// - ctx context.Context
// - ref string
func (_e *MockClonedRepository_Expecter) ReadTree(ctx interface{}, ref interface{}) *MockClonedRepository_ReadTree_Call {
return &MockClonedRepository_ReadTree_Call{Call: _e.mock.On("ReadTree", ctx, ref)}
func (_e *MockStagedRepository_Expecter) ReadTree(ctx interface{}, ref interface{}) *MockStagedRepository_ReadTree_Call {
return &MockStagedRepository_ReadTree_Call{Call: _e.mock.On("ReadTree", ctx, ref)}
}
func (_c *MockClonedRepository_ReadTree_Call) Run(run func(ctx context.Context, ref string)) *MockClonedRepository_ReadTree_Call {
func (_c *MockStagedRepository_ReadTree_Call) Run(run func(ctx context.Context, ref string)) *MockStagedRepository_ReadTree_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *MockClonedRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockClonedRepository_ReadTree_Call {
func (_c *MockStagedRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockStagedRepository_ReadTree_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClonedRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockClonedRepository_ReadTree_Call {
func (_c *MockStagedRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockStagedRepository_ReadTree_Call {
_c.Call.Return(run)
return _c
}
// Remove provides a mock function with given fields: ctx
func (_m *MockClonedRepository) Remove(ctx context.Context) error {
func (_m *MockStagedRepository) Remove(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
@@ -354,36 +353,36 @@ func (_m *MockClonedRepository) Remove(ctx context.Context) error {
return r0
}
// MockClonedRepository_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
type MockClonedRepository_Remove_Call struct {
// MockStagedRepository_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
type MockStagedRepository_Remove_Call struct {
*mock.Call
}
// Remove is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockClonedRepository_Expecter) Remove(ctx interface{}) *MockClonedRepository_Remove_Call {
return &MockClonedRepository_Remove_Call{Call: _e.mock.On("Remove", ctx)}
func (_e *MockStagedRepository_Expecter) Remove(ctx interface{}) *MockStagedRepository_Remove_Call {
return &MockStagedRepository_Remove_Call{Call: _e.mock.On("Remove", ctx)}
}
func (_c *MockClonedRepository_Remove_Call) Run(run func(ctx context.Context)) *MockClonedRepository_Remove_Call {
func (_c *MockStagedRepository_Remove_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Remove_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockClonedRepository_Remove_Call) Return(_a0 error) *MockClonedRepository_Remove_Call {
func (_c *MockStagedRepository_Remove_Call) Return(_a0 error) *MockStagedRepository_Remove_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClonedRepository_Remove_Call) RunAndReturn(run func(context.Context) error) *MockClonedRepository_Remove_Call {
func (_c *MockStagedRepository_Remove_Call) RunAndReturn(run func(context.Context) error) *MockStagedRepository_Remove_Call {
_c.Call.Return(run)
return _c
}
// Test provides a mock function with given fields: ctx
func (_m *MockClonedRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) {
func (_m *MockStagedRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
@@ -412,36 +411,36 @@ func (_m *MockClonedRepository) Test(ctx context.Context) (*v0alpha1.TestResults
return r0, r1
}
// MockClonedRepository_Test_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Test'
type MockClonedRepository_Test_Call struct {
// MockStagedRepository_Test_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Test'
type MockStagedRepository_Test_Call struct {
*mock.Call
}
// Test is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockClonedRepository_Expecter) Test(ctx interface{}) *MockClonedRepository_Test_Call {
return &MockClonedRepository_Test_Call{Call: _e.mock.On("Test", ctx)}
func (_e *MockStagedRepository_Expecter) Test(ctx interface{}) *MockStagedRepository_Test_Call {
return &MockStagedRepository_Test_Call{Call: _e.mock.On("Test", ctx)}
}
func (_c *MockClonedRepository_Test_Call) Run(run func(ctx context.Context)) *MockClonedRepository_Test_Call {
func (_c *MockStagedRepository_Test_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Test_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockClonedRepository_Test_Call) Return(_a0 *v0alpha1.TestResults, _a1 error) *MockClonedRepository_Test_Call {
func (_c *MockStagedRepository_Test_Call) Return(_a0 *v0alpha1.TestResults, _a1 error) *MockStagedRepository_Test_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClonedRepository_Test_Call) RunAndReturn(run func(context.Context) (*v0alpha1.TestResults, error)) *MockClonedRepository_Test_Call {
func (_c *MockStagedRepository_Test_Call) RunAndReturn(run func(context.Context) (*v0alpha1.TestResults, error)) *MockStagedRepository_Test_Call {
_c.Call.Return(run)
return _c
}
// Update provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockClonedRepository) Update(ctx context.Context, path string, ref string, data []byte, message string) error {
func (_m *MockStagedRepository) 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 {
@@ -458,8 +457,8 @@ func (_m *MockClonedRepository) Update(ctx context.Context, path string, ref str
return r0
}
// MockClonedRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
type MockClonedRepository_Update_Call struct {
// MockStagedRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
type MockStagedRepository_Update_Call struct {
*mock.Call
}
@@ -469,29 +468,29 @@ type MockClonedRepository_Update_Call struct {
// - ref string
// - data []byte
// - message string
func (_e *MockClonedRepository_Expecter) Update(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Update_Call {
return &MockClonedRepository_Update_Call{Call: _e.mock.On("Update", ctx, path, ref, data, message)}
func (_e *MockStagedRepository_Expecter) Update(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Update_Call {
return &MockStagedRepository_Update_Call{Call: _e.mock.On("Update", ctx, path, ref, data, message)}
}
func (_c *MockClonedRepository_Update_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Update_Call {
func (_c *MockStagedRepository_Update_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_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 *MockClonedRepository_Update_Call) Return(_a0 error) *MockClonedRepository_Update_Call {
func (_c *MockStagedRepository_Update_Call) Return(_a0 error) *MockStagedRepository_Update_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClonedRepository_Update_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Update_Call {
func (_c *MockStagedRepository_Update_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Update_Call {
_c.Call.Return(run)
return _c
}
// Validate provides a mock function with no fields
func (_m *MockClonedRepository) Validate() field.ErrorList {
func (_m *MockStagedRepository) Validate() field.ErrorList {
ret := _m.Called()
if len(ret) == 0 {
@@ -510,35 +509,35 @@ func (_m *MockClonedRepository) Validate() field.ErrorList {
return r0
}
// MockClonedRepository_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
type MockClonedRepository_Validate_Call struct {
// MockStagedRepository_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
type MockStagedRepository_Validate_Call struct {
*mock.Call
}
// Validate is a helper method to define mock.On call
func (_e *MockClonedRepository_Expecter) Validate() *MockClonedRepository_Validate_Call {
return &MockClonedRepository_Validate_Call{Call: _e.mock.On("Validate")}
func (_e *MockStagedRepository_Expecter) Validate() *MockStagedRepository_Validate_Call {
return &MockStagedRepository_Validate_Call{Call: _e.mock.On("Validate")}
}
func (_c *MockClonedRepository_Validate_Call) Run(run func()) *MockClonedRepository_Validate_Call {
func (_c *MockStagedRepository_Validate_Call) Run(run func()) *MockStagedRepository_Validate_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockClonedRepository_Validate_Call) Return(_a0 field.ErrorList) *MockClonedRepository_Validate_Call {
func (_c *MockStagedRepository_Validate_Call) Return(_a0 field.ErrorList) *MockStagedRepository_Validate_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClonedRepository_Validate_Call) RunAndReturn(run func() field.ErrorList) *MockClonedRepository_Validate_Call {
func (_c *MockStagedRepository_Validate_Call) RunAndReturn(run func() field.ErrorList) *MockStagedRepository_Validate_Call {
_c.Call.Return(run)
return _c
}
// Write provides a mock function with given fields: ctx, path, ref, data, message
func (_m *MockClonedRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error {
func (_m *MockStagedRepository) 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 {
@@ -555,8 +554,8 @@ func (_m *MockClonedRepository) Write(ctx context.Context, path string, ref stri
return r0
}
// MockClonedRepository_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockClonedRepository_Write_Call struct {
// MockStagedRepository_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockStagedRepository_Write_Call struct {
*mock.Call
}
@@ -566,34 +565,34 @@ type MockClonedRepository_Write_Call struct {
// - ref string
// - data []byte
// - message string
func (_e *MockClonedRepository_Expecter) Write(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Write_Call {
return &MockClonedRepository_Write_Call{Call: _e.mock.On("Write", ctx, path, ref, data, message)}
func (_e *MockStagedRepository_Expecter) Write(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Write_Call {
return &MockStagedRepository_Write_Call{Call: _e.mock.On("Write", ctx, path, ref, data, message)}
}
func (_c *MockClonedRepository_Write_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Write_Call {
func (_c *MockStagedRepository_Write_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_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 *MockClonedRepository_Write_Call) Return(_a0 error) *MockClonedRepository_Write_Call {
func (_c *MockStagedRepository_Write_Call) Return(_a0 error) *MockStagedRepository_Write_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClonedRepository_Write_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Write_Call {
func (_c *MockStagedRepository_Write_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Write_Call {
_c.Call.Return(run)
return _c
}
// NewMockClonedRepository creates a new instance of MockClonedRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// NewMockStagedRepository creates a new instance of MockStagedRepository. 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 NewMockClonedRepository(t interface {
func NewMockStagedRepository(t interface {
mock.TestingT
Cleanup(func())
}) *MockClonedRepository {
mock := &MockClonedRepository{}
}) *MockStagedRepository {
mock := &MockStagedRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })

View File

@@ -0,0 +1,144 @@
package repository
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockStagedRepo struct {
*MockStageableRepository
*MockStagedRepository
}
func Test_WrapWithStageAndPushIfPossible_NonStageableRepository(t *testing.T) {
nonStageable := NewMockRepository(t)
var called bool
fn := func(repo Repository, staged bool) error {
called = true
return errors.New("operation failed")
}
err := WrapWithStageAndPushIfPossible(context.Background(), nonStageable, StageOptions{}, fn)
require.EqualError(t, err, "operation failed")
require.True(t, called)
}
func TestWrapWithStageAndPushIfPossible(t *testing.T) {
tests := []struct {
name string
setupMocks func(t *testing.T) *mockStagedRepo
operation func(repo Repository, staged bool) error
expectedError string
}{
{
name: "successful stage, operation, and push",
setupMocks: func(t *testing.T) *mockStagedRepo {
mockRepo := NewMockStageableRepository(t)
mockStaged := NewMockStagedRepository(t)
mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil)
mockStaged.EXPECT().Push(mock.Anything).Return(nil)
mockStaged.EXPECT().Remove(mock.Anything).Return(nil)
return &mockStagedRepo{
MockStageableRepository: mockRepo,
MockStagedRepository: mockStaged,
}
},
operation: func(repo Repository, staged bool) error {
require.True(t, staged)
return nil
},
},
{
name: "stage failure",
setupMocks: func(t *testing.T) *mockStagedRepo {
mockRepo := NewMockStageableRepository(t)
mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(nil, errors.New("stage failed"))
return &mockStagedRepo{
MockStageableRepository: mockRepo,
}
},
operation: func(repo Repository, staged bool) error {
return nil
},
expectedError: "stage repository: stage failed",
},
{
name: "operation failure",
setupMocks: func(t *testing.T) *mockStagedRepo {
mockRepo := NewMockStageableRepository(t)
mockStaged := NewMockStagedRepository(t)
mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil)
mockStaged.EXPECT().Remove(mock.Anything).Return(nil)
return &mockStagedRepo{
MockStageableRepository: mockRepo,
MockStagedRepository: mockStaged,
}
},
operation: func(repo Repository, staged bool) error {
return errors.New("operation failed")
},
expectedError: "operation failed",
},
{
name: "push failure",
setupMocks: func(t *testing.T) *mockStagedRepo {
mockRepo := NewMockStageableRepository(t)
mockStaged := NewMockStagedRepository(t)
mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil)
mockStaged.EXPECT().Push(mock.Anything).Return(errors.New("push failed"))
mockStaged.EXPECT().Remove(mock.Anything).Return(nil)
return &mockStagedRepo{
MockStageableRepository: mockRepo,
MockStagedRepository: mockStaged,
}
},
operation: func(repo Repository, staged bool) error {
return nil
},
expectedError: "wrapped push error: push failed",
},
{
name: "remove failure should only log",
setupMocks: func(t *testing.T) *mockStagedRepo {
mockRepo := NewMockStageableRepository(t)
mockStaged := NewMockStagedRepository(t)
mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil)
mockStaged.EXPECT().Push(mock.Anything).Return(nil)
mockStaged.EXPECT().Remove(mock.Anything).Return(errors.New("remove failed"))
return &mockStagedRepo{
MockStageableRepository: mockRepo,
MockStagedRepository: mockStaged,
}
},
operation: func(repo Repository, staged bool) error {
return nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := tt.setupMocks(t)
err := WrapWithStageAndPushIfPossible(context.Background(), repo, StageOptions{}, tt.operation)
if tt.expectedError != "" {
require.EqualError(t, err, tt.expectedError)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -96,7 +96,7 @@ func ValidateRepository(repo Repository) field.ErrorList {
return list
}
func fromFieldError(err *field.Error) *provisioning.TestResults {
func FromFieldError(err *field.Error) *provisioning.TestResults {
return &provisioning.TestResults{
Code: http.StatusBadRequest,
Success: false,

View File

@@ -431,7 +431,7 @@ func TestFromFieldError(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := fromFieldError(tt.fieldError)
result := FromFieldError(tt.fieldError)
require.NotNil(t, result)
require.Equal(t, tt.expectedCode, result.Code)

View File

@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
// Code generated by mockery v2.52.4. DO NOT EDIT.
package repository

View File

@@ -6,6 +6,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
@@ -76,9 +77,8 @@ func (r *DualReadWriter) Delete(ctx context.Context, opts DualWriteOptions) (*Pa
return nil, err
}
// TODO: implement this
if safepath.IsDir(opts.Path) {
return nil, fmt.Errorf("folder delete not supported")
return r.deleteFolder(ctx, opts)
}
// Read the file from the default branch as it won't exist in the possibly new branch
@@ -166,6 +166,12 @@ func (r *DualReadWriter) CreateFolder(ctx context.Context, opts DualWriteOptions
},
}
urls, err := getFolderURLs(ctx, opts.Path, opts.Ref, r.repo)
if err != nil {
return nil, err
}
wrap.URLs = urls
if opts.Ref == "" {
folderName, err := r.folders.EnsureFolderPathExist(ctx, opts.Path)
if err != nil {
@@ -317,3 +323,141 @@ func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, _ string) er
return apierrors.NewForbidden(FolderResource.GroupResource(), "",
fmt.Errorf("must be admin or editor to access folders with provisioning"))
}
func (r *DualReadWriter) deleteFolder(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) {
// if the ref is set, it is not the active branch, so just delete the files from the branch
// and do not delete the items from grafana itself
if opts.Ref != "" {
err := r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message)
if err != nil {
return nil, fmt.Errorf("error deleting folder from repository: %w", err)
}
return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo)
}
// before deleting from the repo, first get all children resources to delete from grafana afterwards
treeEntries, err := r.repo.ReadTree(ctx, "")
if err != nil {
return nil, fmt.Errorf("read repository tree: %w", err)
}
// note: parsedFolders will include the folder itself
parsedResources, parsedFolders, err := r.getChildren(ctx, opts.Path, treeEntries)
if err != nil {
return nil, fmt.Errorf("parse resources in folder: %w", err)
}
// delete from the repo
err = r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message)
if err != nil {
return nil, fmt.Errorf("delete folder from repository: %w", err)
}
// delete from grafana
ctx, _, err = identity.WithProvisioningIdentity(ctx, r.repo.Config().Namespace)
if err != nil {
return nil, err
}
if err := r.deleteChildren(ctx, parsedResources, parsedFolders); err != nil {
return nil, fmt.Errorf("delete folder from grafana: %w", err)
}
return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo)
}
func getFolderURLs(ctx context.Context, path, ref string, repo repository.Repository) (*provisioning.ResourceURLs, error) {
if urlRepo, ok := repo.(repository.RepositoryWithURLs); ok && ref != "" {
urls, err := urlRepo.ResourceURLs(ctx, &repository.FileInfo{Path: path, Ref: ref})
if err != nil {
return nil, err
}
return urls, nil
}
return nil, nil
}
func folderDeleteResponse(ctx context.Context, path, ref string, repo repository.Repository) (*ParsedResource, error) {
urls, err := getFolderURLs(ctx, path, ref, repo)
if err != nil {
return nil, err
}
parsed := &ParsedResource{
Action: provisioning.ResourceActionDelete,
Info: &repository.FileInfo{
Path: path,
Ref: ref,
},
GVK: schema.GroupVersionKind{
Group: FolderResource.Group,
Version: FolderResource.Version,
Kind: "Folder",
},
GVR: FolderResource,
Repo: provisioning.ResourceRepositoryInfo{
Type: repo.Config().Spec.Type,
Namespace: repo.Config().Namespace,
Name: repo.Config().Name,
Title: repo.Config().Spec.Title,
},
URLs: urls,
}
return parsed, nil
}
func (r *DualReadWriter) getChildren(ctx context.Context, folderPath string, treeEntries []repository.FileTreeEntry) ([]*ParsedResource, []Folder, error) {
var resourcesInFolder []repository.FileTreeEntry
var foldersInFolder []Folder
for _, entry := range treeEntries {
// make sure the path is supported (i.e. not ignored by git sync) and that the path is the folder itself or a child of the folder
if IsPathSupported(entry.Path) != nil || !safepath.InDir(entry.Path, folderPath) {
continue
}
// folders cannot be parsed as resources, so handle them separately
if entry.Blob {
resourcesInFolder = append(resourcesInFolder, entry)
} else {
folder := ParseFolder(entry.Path, r.repo.Config().Name)
foldersInFolder = append(foldersInFolder, folder)
}
}
parsedResources := make([]*ParsedResource, len(resourcesInFolder))
for i, entry := range resourcesInFolder {
fileInfo, err := r.repo.Read(ctx, entry.Path, "")
if err != nil && !apierrors.IsNotFound(err) {
return nil, nil, fmt.Errorf("could not find resource in repository: %w", err)
}
parsed, err := r.parser.Parse(ctx, fileInfo)
if err != nil {
return nil, nil, fmt.Errorf("could not parse resource: %w", err)
}
parsedResources[i] = parsed
}
return parsedResources, foldersInFolder, nil
}
func (r *DualReadWriter) deleteChildren(ctx context.Context, childrenResources []*ParsedResource, folders []Folder) error {
for _, parsed := range childrenResources {
err := parsed.Client.Delete(ctx, parsed.Obj.GetName(), metav1.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to delete nested resource from grafana: %w", err)
}
}
// we need to delete the folders furthest down in the tree first, as folder deletion will fail if there is anything inside of it
safepath.SortByDepth(folders, func(f Folder) string { return f.Path }, false)
for _, f := range folders {
err := r.folders.Client().Delete(ctx, f.ID, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("failed to delete folder from grafana: %w", err)
}
}
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"slices"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -121,7 +122,7 @@ func (r *ResourcesManager) WriteResourceFileFromObject(ctx context.Context, obj
err = r.repo.Write(ctx, fileName, options.Ref, body, commitMessage)
if err != nil {
return "", fmt.Errorf("failed to write file: %w", err)
return "", fmt.Errorf("failed to write file: %s, %w", fileName, err)
}
return fileName, nil
@@ -206,6 +207,10 @@ func (r *ResourcesManager) RemoveResourceFromFile(ctx context.Context, path stri
err = client.Delete(ctx, objName, metav1.DeleteOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return objName, schema.GroupVersionKind{}, nil // Already deleted or simply non-existing, nothing to do
}
return "", schema.GroupVersionKind{}, fmt.Errorf("failed to delete: %w", err)
}

View File

@@ -3,7 +3,6 @@ package resources
import (
"context"
"fmt"
"sort"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -95,10 +94,8 @@ func (t *folderTree) Walk(ctx context.Context, fn WalkFunc) error {
toWalk = append(toWalk, folder)
}
// sort by depth of the paths
sort.Slice(toWalk, func(i, j int) bool {
return safepath.Depth(toWalk[i].Path) < safepath.Depth(toWalk[j].Path)
})
// sort by depth (shallowest first)
safepath.SortByDepth(toWalk, func(f Folder) string { return f.Path }, true)
for _, folder := range toWalk {
if err := fn(ctx, folder, t.tree[folder.ID]); err != nil {

View File

@@ -3,6 +3,7 @@ package safepath
import (
"context"
"path"
"sort"
"strings"
)
@@ -43,3 +44,22 @@ func Split(p string) []string {
}
return strings.Split(trimmed, "/")
}
// SortByDepth will sort any resource, by its path depth. You must pass in
// a way to get said path. Ties are alphabetical by default.
func SortByDepth[T any](items []T, pathExtractor func(T) string, asc bool) {
sort.Slice(items, func(i, j int) bool {
pathI, pathJ := pathExtractor(items[i]), pathExtractor(items[j])
depthI, depthJ := Depth(pathI), Depth(pathJ)
if depthI == depthJ {
// alphabetical by default if depth is the same
return pathI < pathJ
}
if asc {
return depthI < depthJ
}
return depthI > depthJ
})
}

Some files were not shown because too many files have changed in this diff Show More