mirror of
https://github.com/grafana/grafana.git
synced 2026-02-18 18:20:52 -05:00
Provisioning: Wire up repo and connection factory in controllers (#117810)
* Provisioning: Wire up repo and connection factory in controllers * addressing comments - updating the way extras are built in operators * addressing comments * re-adding a switch case
This commit is contained in:
parent
ba9df713c6
commit
7e63391bc2
3 changed files with 499 additions and 72 deletions
|
|
@ -57,9 +57,11 @@ type ControllerConfig struct {
|
|||
decryptService decrypt.DecryptService
|
||||
registry prometheus.Registerer
|
||||
repositoryFactory repository.Factory
|
||||
RepositoryFactoryFunc func() (repository.Factory, error)
|
||||
repositoryExtras []repository.Extra
|
||||
RepositoryExtrasFunc func() ([]repository.Extra, error)
|
||||
connectionFactory connection.Factory
|
||||
ConnectionFactoryFunc func() (connection.Factory, error)
|
||||
connectionExtras []connection.Extra
|
||||
ConnectionExtrasFunc func() ([]connection.Extra, error)
|
||||
healthMetricsRecorder controller.HealthMetricsRecorder
|
||||
tracer tracing.Tracer
|
||||
quotaGetter quotas.QuotaGetter
|
||||
|
|
@ -424,23 +426,20 @@ func (c *ControllerConfig) RepositoryFactory() (repository.Factory, error) {
|
|||
return c.repositoryFactory, nil
|
||||
}
|
||||
|
||||
if c.RepositoryFactoryFunc != nil {
|
||||
repositoryFactory, err := c.RepositoryFactoryFunc()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get repository factory: %w", err)
|
||||
}
|
||||
c.repositoryFactory = repositoryFactory
|
||||
return repositoryFactory, nil
|
||||
extras, err := c.RepositoryExtras()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptSvc, err := c.DecryptService()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setup decrypt service: %w", err)
|
||||
// Build enabled types from the extras
|
||||
enabledTypes := make(map[provisioning.RepositoryType]struct{})
|
||||
for _, extra := range extras {
|
||||
enabledTypes[extra.Type()] = struct{}{}
|
||||
}
|
||||
|
||||
repositoryFactory, err := setupRepoFactory(c.Settings, repository.ProvideDecrypter(decryptSvc), c.provisioningClient, c.Registry())
|
||||
repositoryFactory, err := repository.ProvideFactory(enabledTypes, extras)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setup repository factory: %w", err)
|
||||
return nil, fmt.Errorf("create repository factory: %w", err)
|
||||
}
|
||||
|
||||
c.repositoryFactory = repositoryFactory
|
||||
|
|
@ -453,23 +452,20 @@ func (c *ControllerConfig) ConnectionFactory() (connection.Factory, error) {
|
|||
return c.connectionFactory, nil
|
||||
}
|
||||
|
||||
if c.ConnectionFactoryFunc != nil {
|
||||
connectionFactory, err := c.ConnectionFactoryFunc()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connection factory: %w", err)
|
||||
}
|
||||
c.connectionFactory = connectionFactory
|
||||
return connectionFactory, nil
|
||||
extras, err := c.ConnectionExtras()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptSvc, err := c.DecryptService()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setup decrypt service: %w", err)
|
||||
// Build enabled types from the extras
|
||||
enabledTypes := make(map[provisioning.ConnectionType]struct{})
|
||||
for _, extra := range extras {
|
||||
enabledTypes[extra.Type()] = struct{}{}
|
||||
}
|
||||
|
||||
connectionFactory, err := setupConnectionFactory(c.Settings, connection.ProvideDecrypter(decryptSvc))
|
||||
connectionFactory, err := connection.ProvideFactory(enabledTypes, extras)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setup connection factory: %w", err)
|
||||
return nil, fmt.Errorf("create connection factory: %w", err)
|
||||
}
|
||||
|
||||
c.connectionFactory = connectionFactory
|
||||
|
|
@ -508,30 +504,36 @@ func (c *ControllerConfig) URLProvider() (func(ctx context.Context, namespace st
|
|||
return c.urlProvider, nil
|
||||
}
|
||||
|
||||
func setupRepoFactory(
|
||||
cfg *setting.Cfg,
|
||||
decrypter repository.Decrypter,
|
||||
_ *client.Clientset,
|
||||
registry prometheus.Registerer,
|
||||
) (repository.Factory, error) {
|
||||
operatorSec := cfg.SectionWithEnvOverrides("operator")
|
||||
provisioningSec := cfg.SectionWithEnvOverrides("provisioning")
|
||||
func (c *ControllerConfig) RepositoryExtras() ([]repository.Extra, error) {
|
||||
if c.repositoryExtras != nil {
|
||||
return c.repositoryExtras, nil
|
||||
}
|
||||
|
||||
if c.RepositoryExtrasFunc != nil {
|
||||
extras, err := c.RepositoryExtrasFunc()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get repository extras: %w", err)
|
||||
}
|
||||
c.repositoryExtras = extras
|
||||
return extras, nil
|
||||
}
|
||||
|
||||
// Default OSS implementation
|
||||
decryptSvc, err := c.DecryptService()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get decrypt service: %w", err)
|
||||
}
|
||||
decrypter := repository.ProvideDecrypter(decryptSvc)
|
||||
|
||||
operatorSec := c.Settings.SectionWithEnvOverrides("operator")
|
||||
provisioningSec := c.Settings.SectionWithEnvOverrides("provisioning")
|
||||
repoTypes := provisioningSec.Key("repository_types").Strings("|")
|
||||
if len(repoTypes) == 0 {
|
||||
repoTypes = []string{"github"}
|
||||
}
|
||||
|
||||
// TODO: This depends on the different flavor of Grafana
|
||||
// https://github.com/grafana/git-ui-sync-project/issues/495
|
||||
extras := make([]repository.Extra, 0)
|
||||
alreadyRegistered := make(map[provisioning.RepositoryType]struct{})
|
||||
|
||||
for _, t := range repoTypes {
|
||||
if _, ok := alreadyRegistered[provisioning.RepositoryType(t)]; ok {
|
||||
continue
|
||||
}
|
||||
alreadyRegistered[provisioning.RepositoryType(t)] = struct{}{}
|
||||
|
||||
switch provisioning.RepositoryType(t) {
|
||||
case provisioning.GitRepositoryType:
|
||||
extras = append(extras, gitrepo.Extra(decrypter))
|
||||
|
|
@ -539,57 +541,55 @@ func setupRepoFactory(
|
|||
var webhook *webhooks.WebhookExtraBuilder
|
||||
provisioningAppURL := operatorSec.Key("provisioning_server_public_url").String()
|
||||
if provisioningAppURL != "" {
|
||||
webhook = webhooks.ProvideWebhooks(provisioningAppURL, registry)
|
||||
webhook = webhooks.ProvideWebhooks(provisioningAppURL, c.Registry())
|
||||
}
|
||||
|
||||
extras = append(extras, githubrepo.Extra(decrypter, githubrepo.ProvideFactory(), webhook))
|
||||
case provisioning.LocalRepositoryType:
|
||||
homePath := operatorSec.Key("home_path").String()
|
||||
if homePath == "" {
|
||||
return nil, fmt.Errorf("home_path is required in [operator] section for local repository type")
|
||||
}
|
||||
|
||||
permittedPrefixes := operatorSec.Key("local_permitted_prefixes").Strings("|")
|
||||
if len(permittedPrefixes) == 0 {
|
||||
return nil, fmt.Errorf("local_permitted_prefixes is required in [operator] section for local repository type")
|
||||
}
|
||||
|
||||
extras = append(extras, local.Extra(
|
||||
homePath,
|
||||
permittedPrefixes,
|
||||
))
|
||||
extras = append(extras, local.Extra(homePath, permittedPrefixes))
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported repository type: %s", t)
|
||||
}
|
||||
}
|
||||
|
||||
repoFactory, err := repository.ProvideFactory(alreadyRegistered, extras)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create repository factory: %w", err)
|
||||
}
|
||||
|
||||
return repoFactory, nil
|
||||
c.repositoryExtras = extras
|
||||
return extras, nil
|
||||
}
|
||||
|
||||
func setupConnectionFactory(
|
||||
cfg *setting.Cfg,
|
||||
decrypter connection.Decrypter,
|
||||
) (connection.Factory, error) {
|
||||
// For now, only support GitHub connections
|
||||
// TODO: Add support for other connection types
|
||||
func (c *ControllerConfig) ConnectionExtras() ([]connection.Extra, error) {
|
||||
if c.connectionExtras != nil {
|
||||
return c.connectionExtras, nil
|
||||
}
|
||||
|
||||
if c.ConnectionExtrasFunc != nil {
|
||||
extras, err := c.ConnectionExtrasFunc()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get connection extras: %w", err)
|
||||
}
|
||||
c.connectionExtras = extras
|
||||
return extras, nil
|
||||
}
|
||||
|
||||
// Default OSS implementation
|
||||
decryptSvc, err := c.DecryptService()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get decrypt service: %w", err)
|
||||
}
|
||||
decrypter := connection.ProvideDecrypter(decryptSvc)
|
||||
|
||||
extras := []connection.Extra{
|
||||
githubconnection.Extra(decrypter, githubconnection.ProvideFactory()),
|
||||
}
|
||||
enabledTypes := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
}
|
||||
|
||||
connectionFactory, err := connection.ProvideFactory(enabledTypes, extras)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create connection factory: %w", err)
|
||||
}
|
||||
|
||||
return connectionFactory, nil
|
||||
c.connectionExtras = extras
|
||||
return extras, nil
|
||||
}
|
||||
|
||||
func setupDecryptService(cfg *setting.Cfg, tracer tracing.Tracer, tokenExchangeClient *authn.TokenExchangeClient) (decrypt.DecryptService, error) {
|
||||
|
|
|
|||
|
|
@ -2274,6 +2274,217 @@ func TestIntegrationConnectionController_GranularConditionReasons(t *testing.T)
|
|||
})
|
||||
}
|
||||
|
||||
func TestIntegrationConnectionController_EnterpriseWiring(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
if !extensions.IsEnterprise {
|
||||
t.Skip("Skipping integration test when not enterprise")
|
||||
}
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("GitLab connection can be created and reconciled", func(t *testing.T) {
|
||||
clientSecret := base64.StdEncoding.EncodeToString([]byte("test-client-secret"))
|
||||
|
||||
connection := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-gitlab-connection",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test GitLab Connection",
|
||||
"type": string(provisioning.GitlabConnectionType),
|
||||
"url": "https://gitlab.com",
|
||||
"gitlab": map[string]any{
|
||||
"clientID": "test-client-id",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"clientSecret": map[string]any{
|
||||
"create": clientSecret,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
// CREATE
|
||||
created, err := helper.Connections.Resource.Create(ctx, connection, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "failed to create GitLab connection")
|
||||
require.NotNil(t, created)
|
||||
|
||||
connectionName := created.GetName()
|
||||
require.NotEmpty(t, connectionName, "connection name should not be empty")
|
||||
|
||||
// Cleanup
|
||||
defer func() {
|
||||
_ = helper.Connections.Resource.Delete(ctx, connectionName, metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
// READ
|
||||
output, err := helper.Connections.Resource.Get(ctx, connectionName, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back GitLab connection")
|
||||
assert.Equal(t, connectionName, output.GetName(), "name should be equal")
|
||||
|
||||
spec := output.Object["spec"].(map[string]any)
|
||||
assert.Equal(t, string(provisioning.GitlabConnectionType), spec["type"], "type should be gitlab")
|
||||
|
||||
// Get typed client for status checks
|
||||
restConfig := helper.Org1.Admin.NewRestConfig()
|
||||
provClient, err := clientset.NewForConfig(restConfig)
|
||||
require.NoError(t, err, "failed to create provisioning client")
|
||||
connClient := provClient.ProvisioningV0alpha1().Connections("default")
|
||||
|
||||
// Wait for reconciliation - controller should process the resource
|
||||
// With fake credentials, health check will fail, but reconciliation should happen
|
||||
require.Eventually(t, func() bool {
|
||||
updated, err := connClient.Get(ctx, connectionName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Check that controller has reconciled (ObservedGeneration matches Generation)
|
||||
// and that health check was attempted (Checked > 0)
|
||||
return updated.Status.ObservedGeneration == updated.Generation &&
|
||||
updated.Status.Health.Checked > 0
|
||||
}, 15*time.Second, 500*time.Millisecond, "connection should be reconciled by controller")
|
||||
|
||||
// Verify reconciliation status
|
||||
reconciled, err := connClient.Get(ctx, connectionName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Controller should have set ObservedGeneration - this proves reconciliation happened
|
||||
assert.Equal(t, reconciled.Generation, reconciled.Status.ObservedGeneration,
|
||||
"controller should have reconciled the connection")
|
||||
|
||||
// Health check should have been attempted - proves the controller processed it
|
||||
assert.Greater(t, reconciled.Status.Health.Checked, int64(0),
|
||||
"health check should have been attempted")
|
||||
|
||||
// Should have a ready condition - proves status was updated
|
||||
readyCondition := meta.FindStatusCondition(reconciled.Status.Conditions, provisioning.ConditionTypeReady)
|
||||
assert.NotNil(t, readyCondition, "should have ready condition")
|
||||
|
||||
t.Logf("GitLab connection reconciled successfully. Health: %v, ObservedGen: %d, Checked: %d",
|
||||
reconciled.Status.Health.Healthy, reconciled.Status.ObservedGeneration, reconciled.Status.Health.Checked)
|
||||
})
|
||||
|
||||
t.Run("Bitbucket connection can be created and reconciled", func(t *testing.T) {
|
||||
clientSecret := base64.StdEncoding.EncodeToString([]byte("test-client-secret"))
|
||||
|
||||
connection := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-bitbucket-connection",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test Bitbucket Connection",
|
||||
"type": string(provisioning.BitbucketConnectionType),
|
||||
"bitbucket": map[string]any{
|
||||
"clientID": "test-client-id",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"clientSecret": map[string]any{
|
||||
"create": clientSecret,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
// CREATE
|
||||
created, err := helper.Connections.Resource.Create(ctx, connection, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "failed to create Bitbucket connection")
|
||||
require.NotNil(t, created)
|
||||
|
||||
connectionName := created.GetName()
|
||||
require.NotEmpty(t, connectionName, "connection name should not be empty")
|
||||
|
||||
// Cleanup
|
||||
defer func() {
|
||||
_ = helper.Connections.Resource.Delete(ctx, connectionName, metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
// READ
|
||||
output, err := helper.Connections.Resource.Get(ctx, connectionName, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back Bitbucket connection")
|
||||
assert.Equal(t, connectionName, output.GetName(), "name should be equal")
|
||||
|
||||
spec := output.Object["spec"].(map[string]any)
|
||||
assert.Equal(t, string(provisioning.BitbucketConnectionType), spec["type"], "type should be bitbucket")
|
||||
|
||||
// Get typed client for status checks
|
||||
restConfig := helper.Org1.Admin.NewRestConfig()
|
||||
provClient, err := clientset.NewForConfig(restConfig)
|
||||
require.NoError(t, err, "failed to create provisioning client")
|
||||
connClient := provClient.ProvisioningV0alpha1().Connections("default")
|
||||
|
||||
// Wait for reconciliation
|
||||
require.Eventually(t, func() bool {
|
||||
updated, err := connClient.Get(ctx, connectionName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return updated.Status.ObservedGeneration == updated.Generation &&
|
||||
updated.Status.Health.Checked > 0
|
||||
}, 15*time.Second, 500*time.Millisecond, "connection should be reconciled by controller")
|
||||
|
||||
// Verify reconciliation status
|
||||
reconciled, err := connClient.Get(ctx, connectionName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, reconciled.Generation, reconciled.Status.ObservedGeneration,
|
||||
"controller should have reconciled the connection")
|
||||
assert.Greater(t, reconciled.Status.Health.Checked, int64(0),
|
||||
"health check should have been attempted")
|
||||
|
||||
readyCondition := meta.FindStatusCondition(reconciled.Status.Conditions, provisioning.ConditionTypeReady)
|
||||
assert.NotNil(t, readyCondition, "should have ready condition")
|
||||
|
||||
t.Logf("Bitbucket connection reconciled successfully. Health: %v, ObservedGen: %d, Checked: %d",
|
||||
reconciled.Status.Health.Healthy, reconciled.Status.ObservedGeneration, reconciled.Status.Health.Checked)
|
||||
})
|
||||
|
||||
t.Run("All connection types are supported", func(t *testing.T) {
|
||||
// List all supported connection types by attempting to create connections
|
||||
// This validates the factory has all expected types registered
|
||||
|
||||
supportedTypes := []provisioning.ConnectionType{
|
||||
provisioning.GithubConnectionType,
|
||||
provisioning.GitlabConnectionType,
|
||||
provisioning.BitbucketConnectionType,
|
||||
}
|
||||
|
||||
for _, connType := range supportedTypes {
|
||||
t.Run(string(connType), func(t *testing.T) {
|
||||
// We just check that we can create the object without factory errors
|
||||
// Validation errors are expected if credentials are missing/invalid
|
||||
conn := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"generateName": "test-",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test Connection",
|
||||
"type": string(connType),
|
||||
},
|
||||
}}
|
||||
|
||||
// Try to create - we expect validation error, not "type not supported"
|
||||
_, err := helper.Connections.Resource.Create(ctx, conn, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
// Should be a validation error, not "type not supported"
|
||||
assert.NotContains(t, err.Error(), "is not supported",
|
||||
"type %s should be supported by factory", connType)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func verifyToken(t *testing.T, appID, token string) (bool, error) {
|
||||
t.Helper()
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -1773,3 +1774,218 @@ func TestIntegrationRepositoryController_DefaultBranch(t *testing.T) {
|
|||
}, 30*time.Second, 1*time.Second, "repository should have default branch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationRepositoryController_EnterpriseWiring(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
if !extensions.IsEnterprise {
|
||||
t.Skip("Skipping integration test when not enterprise")
|
||||
}
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("GitLab repository can be created and reconciled", func(t *testing.T) {
|
||||
token := base64.StdEncoding.EncodeToString([]byte("test-gitlab-token"))
|
||||
|
||||
repository := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Repository",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-gitlab-repo",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test GitLab Repository",
|
||||
"type": string(provisioning.GitLabRepositoryType),
|
||||
"gitlab": map[string]any{
|
||||
"url": "https://gitlab.com/test/repo.git",
|
||||
"branch": "main",
|
||||
"path": "dashboards",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"token": map[string]any{
|
||||
"create": token,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
// CREATE
|
||||
created, err := helper.Repositories.Resource.Create(ctx, repository, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "failed to create GitLab repository")
|
||||
require.NotNil(t, created)
|
||||
|
||||
repoName := created.GetName()
|
||||
require.NotEmpty(t, repoName, "repository name should not be empty")
|
||||
|
||||
// Cleanup
|
||||
defer func() {
|
||||
_ = helper.Repositories.Resource.Delete(ctx, repoName, metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
// READ
|
||||
output, err := helper.Repositories.Resource.Get(ctx, repoName, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back GitLab repository")
|
||||
assert.Equal(t, repoName, output.GetName(), "name should be equal")
|
||||
|
||||
spec := output.Object["spec"].(map[string]any)
|
||||
assert.Equal(t, string(provisioning.GitLabRepositoryType), spec["type"], "type should be gitlab")
|
||||
|
||||
// Get typed client for status checks
|
||||
restConfig := helper.Org1.Admin.NewRestConfig()
|
||||
provClient, err := clientset.NewForConfig(restConfig)
|
||||
require.NoError(t, err, "failed to create provisioning client")
|
||||
repoClient := provClient.ProvisioningV0alpha1().Repositories("default")
|
||||
|
||||
// Wait for reconciliation - controller should process the resource
|
||||
// With fake credentials, the git operations will fail, but reconciliation should happen
|
||||
require.Eventually(t, func() bool {
|
||||
updated, err := repoClient.Get(ctx, repoName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Check that controller has reconciled (ObservedGeneration matches Generation)
|
||||
// and that health check was attempted (Checked > 0)
|
||||
return updated.Status.ObservedGeneration == updated.Generation &&
|
||||
updated.Status.Health.Checked > 0
|
||||
}, 15*time.Second, 500*time.Millisecond, "repository should be reconciled by controller")
|
||||
|
||||
// Verify reconciliation status
|
||||
reconciled, err := repoClient.Get(ctx, repoName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Controller should have set ObservedGeneration - this proves reconciliation happened
|
||||
assert.Equal(t, reconciled.Generation, reconciled.Status.ObservedGeneration,
|
||||
"controller should have reconciled the repository")
|
||||
|
||||
// Health check should have been attempted - proves the controller processed it
|
||||
assert.Greater(t, reconciled.Status.Health.Checked, int64(0),
|
||||
"health check should have been attempted")
|
||||
|
||||
// Should have a ready condition - proves status was updated
|
||||
readyCondition := meta.FindStatusCondition(reconciled.Status.Conditions, provisioning.ConditionTypeReady)
|
||||
assert.NotNil(t, readyCondition, "should have ready condition")
|
||||
|
||||
t.Logf("GitLab repository reconciled successfully. Health: %v, ObservedGen: %d, Checked: %d",
|
||||
reconciled.Status.Health.Healthy, reconciled.Status.ObservedGeneration, reconciled.Status.Health.Checked)
|
||||
})
|
||||
|
||||
t.Run("Bitbucket repository can be and reconciled", func(t *testing.T) {
|
||||
token := base64.StdEncoding.EncodeToString([]byte("test-bitbucket-token"))
|
||||
|
||||
repository := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Repository",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-bitbucket-repo",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test Bitbucket Repository",
|
||||
"type": string(provisioning.BitbucketRepositoryType),
|
||||
"bitbucket": map[string]any{
|
||||
"url": "https://bitbucket.org/workspace/repo.git",
|
||||
"branch": "main",
|
||||
"path": "dashboards",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"token": map[string]any{
|
||||
"create": token,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
// CREATE
|
||||
created, err := helper.Repositories.Resource.Create(ctx, repository, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "failed to create Bitbucket repository")
|
||||
require.NotNil(t, created)
|
||||
|
||||
repoName := created.GetName()
|
||||
require.NotEmpty(t, repoName, "repository name should not be empty")
|
||||
|
||||
// Cleanup
|
||||
defer func() {
|
||||
_ = helper.Repositories.Resource.Delete(ctx, repoName, metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
// READ
|
||||
output, err := helper.Repositories.Resource.Get(ctx, repoName, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back Bitbucket repository")
|
||||
assert.Equal(t, repoName, output.GetName(), "name should be equal")
|
||||
|
||||
spec := output.Object["spec"].(map[string]any)
|
||||
assert.Equal(t, string(provisioning.BitbucketRepositoryType), spec["type"], "type should be bitbucket")
|
||||
|
||||
// Get typed client for status checks
|
||||
restConfig := helper.Org1.Admin.NewRestConfig()
|
||||
provClient, err := clientset.NewForConfig(restConfig)
|
||||
require.NoError(t, err, "failed to create provisioning client")
|
||||
repoClient := provClient.ProvisioningV0alpha1().Repositories("default")
|
||||
|
||||
// Wait for reconciliation
|
||||
require.Eventually(t, func() bool {
|
||||
updated, err := repoClient.Get(ctx, repoName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return updated.Status.ObservedGeneration == updated.Generation &&
|
||||
updated.Status.Health.Checked > 0
|
||||
}, 15*time.Second, 500*time.Millisecond, "repository should be reconciled by controller")
|
||||
|
||||
// Verify reconciliation status
|
||||
reconciled, err := repoClient.Get(ctx, repoName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, reconciled.Generation, reconciled.Status.ObservedGeneration,
|
||||
"controller should have reconciled the repository")
|
||||
assert.Greater(t, reconciled.Status.Health.Checked, int64(0),
|
||||
"health check should have been attempted")
|
||||
|
||||
readyCondition := meta.FindStatusCondition(reconciled.Status.Conditions, provisioning.ConditionTypeReady)
|
||||
assert.NotNil(t, readyCondition, "should have ready condition")
|
||||
|
||||
t.Logf("Bitbucket repository reconciled successfully. Health: %v, ObservedGen: %d, Checked: %d",
|
||||
reconciled.Status.Health.Healthy, reconciled.Status.ObservedGeneration, reconciled.Status.Health.Checked)
|
||||
})
|
||||
|
||||
t.Run("All repository types are supported", func(t *testing.T) {
|
||||
// List all supported repository types
|
||||
|
||||
supportedTypes := []provisioning.RepositoryType{
|
||||
provisioning.GitHubRepositoryType,
|
||||
provisioning.GitLabRepositoryType,
|
||||
provisioning.BitbucketRepositoryType,
|
||||
provisioning.GitRepositoryType,
|
||||
provisioning.LocalRepositoryType,
|
||||
}
|
||||
|
||||
for _, repoType := range supportedTypes {
|
||||
t.Run(string(repoType), func(t *testing.T) {
|
||||
// We just check that we can create the object without factory errors
|
||||
// Validation errors are expected if configuration is missing/invalid
|
||||
repo := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Repository",
|
||||
"metadata": map[string]any{
|
||||
"generateName": "test-",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test Repository",
|
||||
"type": string(repoType),
|
||||
},
|
||||
}}
|
||||
|
||||
// Try to create - we expect validation error, not "type not supported"
|
||||
_, err := helper.Repositories.Resource.Create(ctx, repo, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
// Should be a validation error, not "type not supported"
|
||||
assert.NotContains(t, err.Error(), "is not supported",
|
||||
"type %s should be supported by factory", repoType)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue