b863acab05
* Fix race condition causing unhealthy repository message to be lost This commit fixes a race condition in the provisioning repository controller where the "Repository is unhealthy" message in the sync status could be lost due to status updates being based on stale repository objects. ## Problem The issue occurred in the `process` function when: 1. Repository object was fetched from cache with old status 2. `RefreshHealth` immediately patched the health status to "unhealthy" 3. `determineSyncStatusOps` used the stale object to check if unhealthy message was already set 4. A second patch operation based on stale data would overwrite the health status update ## Solution Introduced `RefreshHealthWithPatchOps` method that returns patch operations instead of immediately applying them. This allows batching all status updates (health + sync) into a single atomic patch operation, eliminating the race condition. ## Changes - Added `HealthCheckerInterface` for better testability - Added `RefreshHealthWithPatchOps` method to return patch ops without applying - Updated `process` function to batch health and sync status updates - Added comprehensive unit tests for the fix Fixes the issue where unhealthy repositories don't show the "Repository is unhealthy" message in their sync status. * Fix staticcheck lint error: remove unnecessary nil check for slice
188 lines
5.3 KiB
Go
188 lines
5.3 KiB
Go
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
|
|
|
package mocks
|
|
|
|
import (
|
|
context "context"
|
|
|
|
mock "github.com/stretchr/testify/mock"
|
|
|
|
repository "github.com/grafana/grafana/apps/provisioning/pkg/repository"
|
|
|
|
v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
|
)
|
|
|
|
// MockHealthChecker is an autogenerated mock type for the HealthCheckerInterface type
|
|
type MockHealthChecker struct {
|
|
mock.Mock
|
|
}
|
|
|
|
// HasRecentFailure provides a mock function with given fields: healthStatus, failureType
|
|
func (_m *MockHealthChecker) HasRecentFailure(healthStatus v0alpha1.HealthStatus, failureType v0alpha1.HealthFailureType) bool {
|
|
ret := _m.Called(healthStatus, failureType)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for HasRecentFailure")
|
|
}
|
|
|
|
var r0 bool
|
|
if rf, ok := ret.Get(0).(func(v0alpha1.HealthStatus, v0alpha1.HealthFailureType) bool); ok {
|
|
r0 = rf(healthStatus, failureType)
|
|
} else {
|
|
r0 = ret.Get(0).(bool)
|
|
}
|
|
|
|
return r0
|
|
}
|
|
|
|
// RecordFailure provides a mock function with given fields: ctx, failureType, err, repo
|
|
func (_m *MockHealthChecker) RecordFailure(ctx context.Context, failureType v0alpha1.HealthFailureType, err error, repo *v0alpha1.Repository) error {
|
|
ret := _m.Called(ctx, failureType, err, repo)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for RecordFailure")
|
|
}
|
|
|
|
var r0 error
|
|
if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.HealthFailureType, error, *v0alpha1.Repository) error); ok {
|
|
r0 = rf(ctx, failureType, err, repo)
|
|
} else {
|
|
r0 = ret.Error(0)
|
|
}
|
|
|
|
return r0
|
|
}
|
|
|
|
// RefreshHealth provides a mock function with given fields: ctx, repo
|
|
func (_m *MockHealthChecker) RefreshHealth(ctx context.Context, repo repository.Repository) (*v0alpha1.TestResults, v0alpha1.HealthStatus, error) {
|
|
ret := _m.Called(ctx, repo)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for RefreshHealth")
|
|
}
|
|
|
|
var r0 *v0alpha1.TestResults
|
|
var r1 v0alpha1.HealthStatus
|
|
var r2 error
|
|
if rf, ok := ret.Get(0).(func(context.Context, repository.Repository) (*v0alpha1.TestResults, v0alpha1.HealthStatus, error)); ok {
|
|
return rf(ctx, repo)
|
|
}
|
|
if rf, ok := ret.Get(0).(func(context.Context, repository.Repository) *v0alpha1.TestResults); ok {
|
|
r0 = rf(ctx, repo)
|
|
} else {
|
|
if ret.Get(0) != nil {
|
|
r0 = ret.Get(0).(*v0alpha1.TestResults)
|
|
}
|
|
}
|
|
|
|
if rf, ok := ret.Get(1).(func(context.Context, repository.Repository) v0alpha1.HealthStatus); ok {
|
|
r1 = rf(ctx, repo)
|
|
} else {
|
|
r1 = ret.Get(1).(v0alpha1.HealthStatus)
|
|
}
|
|
|
|
if rf, ok := ret.Get(2).(func(context.Context, repository.Repository) error); ok {
|
|
r2 = rf(ctx, repo)
|
|
} else {
|
|
r2 = ret.Error(2)
|
|
}
|
|
|
|
return r0, r1, r2
|
|
}
|
|
|
|
// RefreshHealthWithPatchOps provides a mock function with given fields: ctx, repo
|
|
func (_m *MockHealthChecker) RefreshHealthWithPatchOps(ctx context.Context, repo repository.Repository) (*v0alpha1.TestResults, v0alpha1.HealthStatus, []map[string]interface{}, error) {
|
|
ret := _m.Called(ctx, repo)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for RefreshHealthWithPatchOps")
|
|
}
|
|
|
|
var r0 *v0alpha1.TestResults
|
|
var r1 v0alpha1.HealthStatus
|
|
var r2 []map[string]interface{}
|
|
var r3 error
|
|
if rf, ok := ret.Get(0).(func(context.Context, repository.Repository) (*v0alpha1.TestResults, v0alpha1.HealthStatus, []map[string]interface{}, error)); ok {
|
|
return rf(ctx, repo)
|
|
}
|
|
if rf, ok := ret.Get(0).(func(context.Context, repository.Repository) *v0alpha1.TestResults); ok {
|
|
r0 = rf(ctx, repo)
|
|
} else {
|
|
if ret.Get(0) != nil {
|
|
r0 = ret.Get(0).(*v0alpha1.TestResults)
|
|
}
|
|
}
|
|
|
|
if rf, ok := ret.Get(1).(func(context.Context, repository.Repository) v0alpha1.HealthStatus); ok {
|
|
r1 = rf(ctx, repo)
|
|
} else {
|
|
r1 = ret.Get(1).(v0alpha1.HealthStatus)
|
|
}
|
|
|
|
if rf, ok := ret.Get(2).(func(context.Context, repository.Repository) []map[string]interface{}); ok {
|
|
r2 = rf(ctx, repo)
|
|
} else {
|
|
if ret.Get(2) != nil {
|
|
r2 = ret.Get(2).([]map[string]interface{})
|
|
}
|
|
}
|
|
|
|
if rf, ok := ret.Get(3).(func(context.Context, repository.Repository) error); ok {
|
|
r3 = rf(ctx, repo)
|
|
} else {
|
|
r3 = ret.Error(3)
|
|
}
|
|
|
|
return r0, r1, r2, r3
|
|
}
|
|
|
|
// RefreshTimestamp provides a mock function with given fields: ctx, repo
|
|
func (_m *MockHealthChecker) RefreshTimestamp(ctx context.Context, repo *v0alpha1.Repository) error {
|
|
ret := _m.Called(ctx, repo)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for RefreshTimestamp")
|
|
}
|
|
|
|
var r0 error
|
|
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository) error); ok {
|
|
r0 = rf(ctx, repo)
|
|
} else {
|
|
r0 = ret.Error(0)
|
|
}
|
|
|
|
return r0
|
|
}
|
|
|
|
// ShouldCheckHealth provides a mock function with given fields: repo
|
|
func (_m *MockHealthChecker) ShouldCheckHealth(repo *v0alpha1.Repository) bool {
|
|
ret := _m.Called(repo)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for ShouldCheckHealth")
|
|
}
|
|
|
|
var r0 bool
|
|
if rf, ok := ret.Get(0).(func(*v0alpha1.Repository) bool); ok {
|
|
r0 = rf(repo)
|
|
} else {
|
|
r0 = ret.Get(0).(bool)
|
|
}
|
|
|
|
return r0
|
|
}
|
|
|
|
// NewMockHealthChecker creates a new instance of MockHealthChecker. 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 NewMockHealthChecker(t interface {
|
|
mock.TestingT
|
|
Cleanup(func())
|
|
}) *MockHealthChecker {
|
|
mock := &MockHealthChecker{}
|
|
mock.Mock.Test(t)
|
|
|
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
|
|
|
return mock
|
|
}
|