Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs
This commit is contained in:
@@ -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"]
|
||||
|
||||
5
.github/workflows/frontend-lint.yml
vendored
5
.github/workflows/frontend-lint.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -23,5 +23,7 @@ type CheckTypeSpec struct {
|
||||
|
||||
// NewCheckTypeSpec creates a new CheckTypeSpec object.
|
||||
func NewCheckTypeSpec() *CheckTypeSpec {
|
||||
return &CheckTypeSpec{}
|
||||
return &CheckTypeSpec{
|
||||
Steps: []CheckTypeStep{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ RoleSpec: {
|
||||
|
||||
// Display name of the role
|
||||
title: string
|
||||
description: string
|
||||
|
||||
version: int
|
||||
group: string
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
13
go.mod
@@ -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
32
go.sum
@@ -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=
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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' } })}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
33
pkg/registry/apis/provisioning/repository/git/branch.go
Normal file
33
pkg/registry/apis/provisioning/repository/git/branch.go
Normal 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
|
||||
}
|
||||
47
pkg/registry/apis/provisioning/repository/git/branch_test.go
Normal file
47
pkg/registry/apis/provisioning/repository/git/branch_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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{}))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
240
pkg/registry/apis/provisioning/repository/github/repository.go
Normal file
240
pkg/registry/apis/provisioning/repository/github/repository.go
Normal 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)
|
||||
}
|
||||
1014
pkg/registry/apis/provisioning/repository/github/repository_test.go
Normal file
1014
pkg/registry/apis/provisioning/repository/github/repository_test.go
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
71
pkg/registry/apis/provisioning/repository/staged.go
Normal file
71
pkg/registry/apis/provisioning/repository/staged.go
Normal 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
|
||||
}
|
||||
@@ -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) })
|
||||
144
pkg/registry/apis/provisioning/repository/staged_test.go
Normal file
144
pkg/registry/apis/provisioning/repository/staged_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user