Provisioning: Refactor to combine validation and test endpoint logic (#111965)

Provisioning: Refactor test endpoint
This commit is contained in:
Stephanie Hingtgen
2025-10-03 01:16:37 -06:00
committed by GitHub
parent add8beefad
commit 3ce9137c19
10 changed files with 437 additions and 292 deletions
+23 -2
View File
@@ -11,9 +11,11 @@ import (
"github.com/grafana/grafana-app-sdk/logging"
appcontroller "github.com/grafana/grafana/apps/provisioning/pkg/controller"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/client-go/tools/cache"
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
@@ -66,8 +68,14 @@ func RunRepoController(deps server.OperatorDependencies) error {
if err != nil {
return fmt.Errorf("create API client job store: %w", err)
}
allowedTargets := []v0alpha1.SyncTargetType{}
for _, target := range controllerCfg.allowedTargets {
allowedTargets = append(allowedTargets, v0alpha1.SyncTargetType(target))
}
validator := repository.NewValidator(controllerCfg.minSyncInterval, allowedTargets, controllerCfg.allowImageRendering)
statusPatcher := appcontroller.NewRepositoryStatusPatcher(controllerCfg.provisioningClient.ProvisioningV0alpha1())
healthChecker := controller.NewHealthChecker(statusPatcher, deps.Registerer)
healthChecker := controller.NewHealthChecker(statusPatcher, deps.Registerer, repository.NewSimpleRepositoryTester(validator))
repoInformer := informerFactory.Provisioning().V0alpha1().Repositories()
controller, err := controller.NewRepositoryController(
@@ -98,7 +106,10 @@ func RunRepoController(deps server.OperatorDependencies) error {
type repoControllerConfig struct {
provisioningControllerConfig
workerCount int
workerCount int
allowedTargets []string
allowImageRendering bool
minSyncInterval time.Duration
}
func getRepoControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) (*repoControllerConfig, error) {
@@ -106,8 +117,18 @@ func getRepoControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) (
if err != nil {
return nil, err
}
allowedTargets := []string{}
cfg.SectionWithEnvOverrides("provisioning").Key("allowed_targets").Strings("|")
if len(allowedTargets) == 0 {
allowedTargets = []string{"folder"}
}
return &repoControllerConfig{
provisioningControllerConfig: *controllerCfg,
allowedTargets: allowedTargets,
workerCount: cfg.SectionWithEnvOverrides("operator").Key("worker_count").MustInt(1),
allowImageRendering: cfg.SectionWithEnvOverrides("provisioning").Key("allow_image_rendering").MustBool(false),
minSyncInterval: cfg.SectionWithEnvOverrides("provisioning").Key("min_sync_interval").MustDuration(1 * time.Minute),
}, nil
}
@@ -23,14 +23,16 @@ type StatusPatcher interface {
type HealthChecker struct {
statusPatcher StatusPatcher
healthMetrics healthMetrics
tester repository.SimpleRepositoryTester
}
// NewHealthChecker creates a new health checker
func NewHealthChecker(statusPatcher StatusPatcher, registry prometheus.Registerer) *HealthChecker {
func NewHealthChecker(statusPatcher StatusPatcher, registry prometheus.Registerer, tester repository.SimpleRepositoryTester) *HealthChecker {
healthMetrics := registerHealthMetrics(registry)
return &HealthChecker{
statusPatcher: statusPatcher,
healthMetrics: healthMetrics,
tester: tester,
}
}
@@ -176,7 +178,7 @@ func (hc *HealthChecker) refreshHealth(ctx context.Context, repo repository.Repo
hc.healthMetrics.RecordHealthCheck(outcome, time.Since(start).Seconds())
}()
res, err := repository.TestRepository(ctx, repo)
res, err := hc.tester.TestRepository(ctx, repo)
if err != nil {
outcome = utils.ErrorOutcome
logger.Error("failed to test repository", "error", err)
@@ -13,13 +13,15 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
repository "github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller/mocks"
)
func TestNewHealthChecker(t *testing.T) {
mockPatcher := mocks.NewStatusPatcher(t)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry())
validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator))
assert.NotNil(t, hc)
assert.Equal(t, mockPatcher, hc.statusPatcher)
@@ -136,7 +138,8 @@ func TestShouldCheckHealth(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPatcher := mocks.NewStatusPatcher(t)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry())
validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator))
result := hc.ShouldCheckHealth(tt.repo)
assert.Equal(t, tt.expected, result)
@@ -223,7 +226,8 @@ func TestHasRecentFailure(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPatcher := mocks.NewStatusPatcher(t)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry())
validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator))
result := hc.HasRecentFailure(tt.healthStatus, tt.failureType)
assert.Equal(t, tt.expected, result)
@@ -265,7 +269,8 @@ func TestRecordFailure(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPatcher := mocks.NewStatusPatcher(t)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry())
validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator))
repo := &provisioning.Repository{
Status: provisioning.RepositoryStatus{
@@ -310,7 +315,8 @@ func TestRecordFailure(t *testing.T) {
func TestRecordFailureFunction(t *testing.T) {
mockPatcher := mocks.NewStatusPatcher(t)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry())
validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator))
testErr := errors.New("test error")
result := hc.recordFailure(provisioning.HealthFailureHook, testErr)
@@ -447,7 +453,8 @@ func TestRefreshHealth(t *testing.T) {
testError: tt.testError,
}
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry())
validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator))
if tt.expectPatch {
if tt.patchError != nil {
@@ -557,7 +564,8 @@ func TestHasHealthStatusChanged(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPatcher := mocks.NewStatusPatcher(t)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry())
validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)
hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator))
result := hc.hasHealthStatusChanged(tt.old, tt.new)
assert.Equal(t, tt.expected, result)
+11 -32
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"time"
@@ -92,7 +91,6 @@ type APIBuilder struct {
allowedTargets []provisioning.SyncTargetType
allowImageRendering bool
minSyncInterval time.Duration
features featuremgmt.FeatureToggles
usageStats usagestats.Service
@@ -117,6 +115,7 @@ type APIBuilder struct {
access authlib.AccessChecker
statusPatcher *appcontroller.RepositoryStatusPatcher
healthChecker *controller.HealthChecker
validator repository.RepositoryValidator
// Extras provides additional functionality to the API.
extras []Extra
extraWorkers []jobs.Worker
@@ -158,11 +157,6 @@ func NewAPIBuilder(
parsers := resources.NewParserFactory(clients)
resourceLister := resources.NewResourceListerForMigrations(unified, legacyMigrator, storageStatus)
// do not allow minsync interval to be less than 10
if minSyncInterval <= 10*time.Second {
minSyncInterval = 10 * time.Second
}
b := &APIBuilder{
onlyApiServer: onlyApiServer,
tracer: tracer,
@@ -179,11 +173,11 @@ func NewAPIBuilder(
access: access,
jobHistoryConfig: jobHistoryConfig,
extraWorkers: extraWorkers,
allowedTargets: allowedTargets,
restConfigGetter: restConfigGetter,
allowedTargets: allowedTargets,
allowImageRendering: allowImageRendering,
minSyncInterval: minSyncInterval,
registry: registry,
validator: repository.NewValidator(minSyncInterval, allowedTargets, allowImageRendering),
}
for _, builder := range extraBuilders {
@@ -484,7 +478,7 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
storage[provisioning.RepositoryResourceInfo.StoragePath("status")] = repositoryStatusStorage
// TODO: Add some logic so that the connectors can registered themselves and we don't have logic all over the place
storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b)
storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b, repository.NewRepositoryTesterWithExistingChecker(repository.NewSimpleRepositoryTester(b.validator), b.VerifyAgainstExistingRepositories))
storage[provisioning.RepositoryResourceInfo.StoragePath("files")] = NewFilesConnector(b, b.parsers, b.clients, b.access)
storage[provisioning.RepositoryResourceInfo.StoragePath("refs")] = NewRefsConnector(b)
storage[provisioning.RepositoryResourceInfo.StoragePath("resources")] = &listConnector{
@@ -585,29 +579,14 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm
return err
}
list := repository.ValidateRepository(repo)
// ALL configuration validations should be done in ValidateRepository -
// this is how the UI is able to show proper validation errors
//
// the only time to add configuration checks here is if you need to compare
// the incoming change to the current configuration
list := b.validator.ValidateRepository(repo)
cfg := repo.Config()
if !slices.Contains(b.allowedTargets, cfg.Spec.Sync.Target) {
list = append(list,
field.Invalid(
field.NewPath("spec", "target"),
cfg.Spec.Sync.Target,
"sync target is not supported"))
}
if cfg.Spec.Sync.Enabled && cfg.Spec.Sync.IntervalSeconds < int64(b.minSyncInterval.Seconds()) {
list = append(list, field.Invalid(field.NewPath("spec", "sync", "intervalSeconds"),
cfg.Spec.Sync.IntervalSeconds, fmt.Sprintf("Interval must be at least %d seconds", int64(b.minSyncInterval.Seconds()))))
}
if !b.allowImageRendering && cfg.Spec.GitHub != nil && cfg.Spec.GitHub.GenerateDashboardPreviews {
list = append(list,
field.Invalid(field.NewPath("spec", "generateDashboardPreviews"),
cfg.Spec.GitHub.GenerateDashboardPreviews,
"image rendering is not enabled"))
}
if a.GetOperation() == admission.Update {
oldRepo, err := b.asRepository(ctx, a.GetOldObject(), nil)
if err != nil {
@@ -683,7 +662,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
}
b.statusPatcher = appcontroller.NewRepositoryStatusPatcher(b.GetClient())
b.healthChecker = controller.NewHealthChecker(b.statusPatcher, b.registry)
b.healthChecker = controller.NewHealthChecker(b.statusPatcher, b.registry, repository.NewSimpleRepositoryTester(b.validator))
// if running solely CRUD, skip the rest of the setup
if b.onlyApiServer {
@@ -23,11 +23,12 @@ func TestAPIBuilderValidate(t *testing.T) {
mockRepo := repository.NewMockConfigRepository(t)
mockRepo.EXPECT().Validate().Return(nil)
factory.EXPECT().Build(mock.Anything, mock.Anything).Return(mockRepo, nil)
validator := repository.NewValidator(30*time.Second, []v0alpha1.SyncTargetType{v0alpha1.SyncTargetTypeFolder}, false)
b := &APIBuilder{
repoFactory: factory,
allowedTargets: []v0alpha1.SyncTargetType{v0alpha1.SyncTargetTypeFolder},
allowImageRendering: false,
minSyncInterval: 30 * time.Second,
validator: validator,
}
t.Run("min sync interval is less than 10 seconds", func(t *testing.T) {
+4 -5
View File
@@ -31,7 +31,6 @@ type HealthCheckerProvider interface {
type ConnectorDependencies interface {
RepoGetter
HealthCheckerProvider
repository.RepositoryValidator
GetRepoFactory() repository.Factory
}
@@ -39,15 +38,15 @@ type testConnector struct {
getter RepoGetter
factory repository.Factory
healthProvider HealthCheckerProvider
validator repository.RepositoryValidator
tester repository.RepositoryTesterWithExistingChecker
}
func NewTestConnector(deps ConnectorDependencies) *testConnector {
func NewTestConnector(deps ConnectorDependencies, tester repository.RepositoryTesterWithExistingChecker) *testConnector {
return &testConnector{
factory: deps.GetRepoFactory(),
getter: deps,
healthProvider: deps,
validator: deps,
tester: tester,
}
}
@@ -186,7 +185,7 @@ func (s *testConnector) Connect(ctx context.Context, name string, opts runtime.O
}
} else {
// Testing temporary repository - just run test without status update
rsp, err = repository.TestRepositoryWithValidator(ctx, repo, s.validator)
rsp, err = s.tester.TestRepositoryAndCheckExisting(ctx, repo)
if err != nil {
responder.Error(err)
return