Provisioning: Add connection operator with health check updates (#116028)
* Add connection operator with health check updates - Add ConnectionController to watch and reconcile Connection resources - Add ConnectionStatusPatcher for updating connection status - Add connection_operator.go entry point for standalone operator - Register connection operator in pkg/operators/register.go - Add connection controller to in-process setup in register.go - Add unit tests for connection controller - Add integration tests for health check updates * Fix integration test: get latest version before update to avoid conflicts * refactor: move repoFactory to operator-specific configs - Remove repoFactory from shared provisioningControllerConfig - Add repoFactory to repoControllerConfig and jobsControllerConfig - This allows connection operator to run without repository setup * Remove unneccesary comments
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -11,6 +12,9 @@ import (
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
clientset "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned"
|
||||
)
|
||||
|
||||
func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) {
|
||||
@@ -411,3 +415,147 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "privateKey is forbidden in Gitlab connection")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationConnectionController_HealthCheckUpdates(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
namespace := "default"
|
||||
|
||||
// Create typed client from REST config
|
||||
restConfig := helper.Org1.Admin.NewRestConfig()
|
||||
provisioningClient, err := clientset.NewForConfig(restConfig)
|
||||
require.NoError(t, err)
|
||||
connClient := provisioningClient.ProvisioningV0alpha1().Connections(namespace)
|
||||
|
||||
t.Run("health check gets updated after initial creation", func(t *testing.T) {
|
||||
// Create a connection using unstructured (like other connection tests)
|
||||
connUnstructured := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-connection-health",
|
||||
"namespace": namespace,
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"type": "github",
|
||||
"github": map[string]any{
|
||||
"appID": "12345",
|
||||
"installationID": "67890",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"privateKey": map[string]any{
|
||||
"create": "test-private-key",
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
createdUnstructured, err := helper.Connections.Resource.Create(ctx, connUnstructured, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, createdUnstructured)
|
||||
|
||||
connName := createdUnstructured.GetName()
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = helper.Connections.Resource.Delete(ctx, connName, metav1.DeleteOptions{})
|
||||
})
|
||||
|
||||
// Wait for initial reconciliation - controller should update status
|
||||
require.Eventually(t, func() bool {
|
||||
updated, err := connClient.Get(ctx, connName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return updated.Status.ObservedGeneration == updated.Generation &&
|
||||
updated.Status.Health.Checked > 0 &&
|
||||
updated.Status.State == provisioning.ConnectionStateConnected &&
|
||||
updated.Status.Health.Healthy
|
||||
}, 10*time.Second, 500*time.Millisecond, "connection should be initially reconciled with health status")
|
||||
|
||||
// Verify initial health check was set
|
||||
initial, err := connClient.Get(ctx, connName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, initial.Status.Health.Healthy, "connection should be healthy")
|
||||
assert.Equal(t, provisioning.ConnectionStateConnected, initial.Status.State, "connection should be connected")
|
||||
assert.Greater(t, initial.Status.Health.Checked, int64(0), "health check timestamp should be set")
|
||||
assert.Equal(t, initial.Generation, initial.Status.ObservedGeneration, "observed generation should match")
|
||||
})
|
||||
|
||||
t.Run("health check updates when spec changes", func(t *testing.T) {
|
||||
// Create a connection using unstructured
|
||||
connUnstructured := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-connection-spec-change",
|
||||
"namespace": namespace,
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"type": "github",
|
||||
"github": map[string]any{
|
||||
"appID": "11111",
|
||||
"installationID": "22222",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"privateKey": map[string]any{
|
||||
"create": "test-private-key-2",
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
createdUnstructured, err := helper.Connections.Resource.Create(ctx, connUnstructured, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, createdUnstructured)
|
||||
|
||||
connName := createdUnstructured.GetName()
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = helper.Connections.Resource.Delete(ctx, connName, metav1.DeleteOptions{})
|
||||
})
|
||||
|
||||
// Wait for initial reconciliation
|
||||
var initialHealthChecked int64
|
||||
require.Eventually(t, func() bool {
|
||||
updated, err := connClient.Get(ctx, connName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if updated.Status.ObservedGeneration == updated.Generation {
|
||||
initialHealthChecked = updated.Status.Health.Checked
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, 10*time.Second, 500*time.Millisecond, "connection should be initially reconciled")
|
||||
|
||||
// Get the latest version before updating to avoid conflicts with controller updates
|
||||
latestUnstructured, err := helper.Connections.Resource.Get(ctx, connName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update the connection spec using the latest version
|
||||
updatedUnstructured := latestUnstructured.DeepCopy()
|
||||
githubSpec := updatedUnstructured.Object["spec"].(map[string]any)["github"].(map[string]any)
|
||||
githubSpec["appID"] = "99999"
|
||||
_, err = helper.Connections.Resource.Update(ctx, updatedUnstructured, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for reconciliation after spec change
|
||||
require.Eventually(t, func() bool {
|
||||
reconciled, err := connClient.Get(ctx, connName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return reconciled.Status.ObservedGeneration == reconciled.Generation &&
|
||||
reconciled.Status.Health.Checked > initialHealthChecked
|
||||
}, 10*time.Second, 500*time.Millisecond, "connection should be reconciled after spec change")
|
||||
|
||||
// Verify health check was updated
|
||||
final, err := connClient.Get(ctx, connName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, final.Generation, final.Status.ObservedGeneration, "observed generation should match generation")
|
||||
assert.Greater(t, final.Status.Health.Checked, initialHealthChecked, "health check should be updated after spec change")
|
||||
assert.True(t, final.Status.Health.Healthy, "connection should remain healthy")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user