Files
grafana/pkg/registry/apis/provisioning/controller/mocks/HealthCheckerInterface.go
T
Roberto Jiménez Sánchez b863acab05 Provisioning: Fix race condition causing unhealthy repository message to be lost (#115150)
* 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
2025-12-12 13:24:58 +02:00

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
}