Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-e2e-test-nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Nightly E2E Workflow
on:
workflow_dispatch:
schedule:
- cron: "0 1-6 * * *" # run hourly from 1am to 6am
- cron: "0 1-5/2 * * *" # run every 2 hours from 1am to 5am
jobs:

init:
Expand Down
8 changes: 7 additions & 1 deletion cmd/greenhouse/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var knownControllers = map[string]func(controllerName string, mgr ctrl.Manager)

"catalog": startCatalogReconciler,
"pluginDefinition": startPluginDefinitionReconciler,
"clusterPluginDefinition": (&plugindefinitioncontroller.ClusterPluginDefinitionReconciler{}).SetupWithManager,
"clusterPluginDefinition": startClusterPluginDefinitionReconciler,

// Cluster controllers
"bootstrap": (&clustercontrollers.BootstrapReconciler{}).SetupWithManager,
Expand Down Expand Up @@ -97,6 +97,12 @@ func startPluginDefinitionReconciler(name string, mgr ctrl.Manager) error {
}).SetupWithManager(name, mgr)
}

func startClusterPluginDefinitionReconciler(name string, mgr ctrl.Manager) error {
return (&plugindefinitioncontroller.ClusterPluginDefinitionReconciler{
OCIMirroringEnabled: featureFlags.IsOCIMirroringEnabled(),
}).SetupWithManager(name, mgr)
}

func startClusterReconciler(name string, mgr ctrl.Manager) error {
if renewRemoteClusterBearerTokenAfter > remoteClusterBearerTokenValidity {
setupLog.Info("WARN: remoteClusterBearerTokenValidity is less than renewRemoteClusterBearerTokenAfter")
Expand Down
65 changes: 65 additions & 0 deletions e2e/clusterplugindefinition/e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

//go:build clusterplugindefinitionE2E

package clusterplugindefinition

import (
"context"
"testing"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

"github.com/cloudoperators/greenhouse/e2e/clusterplugindefinition/scenarios"
"github.com/cloudoperators/greenhouse/e2e/shared"
"github.com/cloudoperators/greenhouse/internal/clientutil"
)

var (
env *shared.TestEnv
ctx context.Context
adminClient client.Client
testStartTime time.Time
)

func TestE2e(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "ClusterPluginDefinition E2E Suite")
}

var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
ctx = context.Background()
env = shared.NewExecutionEnv()

var err error
adminClient, err = clientutil.NewK8sClientFromRestClientGetter(env.AdminRestClientGetter)
Expect(err).ToNot(HaveOccurred(), "there should be no error creating the admin client")

env = env.WithOrganization(ctx, adminClient, "./testdata/organization.yaml")
shared.SetupOCIMirroringForOrg(ctx, adminClient, env.TestNamespace)

testStartTime = time.Now().UTC()
})

var _ = AfterSuite(func() {
Comment thread
abhijith-darshan marked this conversation as resolved.
env.GenerateGreenhouseControllerLogs(ctx, testStartTime)
env.GenerateFluxControllerLogs(ctx, "helm-controller", testStartTime)
shared.TeardownOCIMirroringForOrg(ctx, adminClient, env.TestNamespace)
})

var _ = Describe("ClusterPluginDefinition E2E", Ordered, func() {
It("should replicate helm chart to registry mirror", func() {
scenarios.ClusterPDChartReplication(ctx, adminClient)
})

It("should fail chart replication for non-existent chart version", func() {
scenarios.ClusterPDChartReplicationFailure(ctx, adminClient)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package scenarios

import (
"context"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/client"

greenhousev1alpha1 "github.com/cloudoperators/greenhouse/api/v1alpha1"
"github.com/cloudoperators/greenhouse/e2e/shared"
"github.com/cloudoperators/greenhouse/internal/test"
"github.com/cloudoperators/greenhouse/pkg/lifecycle"
)

// ClusterPDChartReplication verifies that the ClusterPluginDefinition controller replicates a Helm chart OCI artifact via the registry mirror configured on the greenhouse Organization.
func ClusterPDChartReplication(ctx context.Context, adminClient client.Client) {
cpd := test.NewClusterPluginDefinition(ctx, "podinfo-cluster-chart-replication",
test.WithVersion("6.7.1"),
test.WithHelmChart(&greenhousev1alpha1.HelmChartReference{
Name: "podinfo",
Repository: "oci://registry:5000/greenhouse-ghcr-io-mirror/stefanprodan/charts",
Version: "6.7.1",
}),
)

By("creating ClusterPluginDefinition with chart URL at registry mirror")
err := adminClient.Create(ctx, cpd)
Expect(client.IgnoreAlreadyExists(err)).ToNot(HaveOccurred())
DeferCleanup(func() { test.EventuallyDeleted(ctx, adminClient, cpd) })

By("verifying OCIReplicationReady condition becomes True with OCIReplicationSucceeded")
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cpd), cpd)).To(Succeed())
cond := cpd.Status.StatusConditions.GetConditionByType(greenhousev1alpha1.OCIReplicationReadyCondition)
g.Expect(cond).NotTo(BeNil(), "OCIReplicationReady condition should be set")
g.Expect(cond.IsTrue()).To(BeTrue(), "chart replication should succeed")
g.Expect(cond.Reason).To(Equal(greenhousev1alpha1.OCIReplicationSucceededReason))
}).WithTimeout(shared.OCIReplicationTimeout).Should(Succeed(), "registry mirror should pull-through the podinfo chart from ghcr.io")

By("verifying LastSyncedArtifact is populated")
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cpd), cpd)).To(Succeed())
artifact := cpd.GetLastSyncedArtifact()
g.Expect(artifact).NotTo(BeNil(), "LastSyncedArtifact should be set after replication")
g.Expect(artifact.Registry).To(Equal("registry:5000"))
g.Expect(artifact.ChartName).To(Equal("greenhouse-ghcr-io-mirror/stefanprodan/charts/podinfo"))
g.Expect(artifact.Version).To(Equal("6.7.1"))
g.Expect(artifact.Digest).NotTo(BeEmpty(), "digest should be populated after successful replication")
g.Expect(artifact.ReplicationStatus).To(Equal(greenhousev1alpha1.ReplicationStatusReplicated))
}).WithTimeout(shared.OCIReplicationTimeout).Should(Succeed())

By("verifying HelmChartReady condition becomes True")
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cpd), cpd)).To(Succeed())
cond := cpd.Status.StatusConditions.GetConditionByType(greenhousev1alpha1.HelmChartReadyCondition)
g.Expect(cond).NotTo(BeNil())
g.Expect(cond.IsTrue()).To(BeTrue(), "Flux should fetch the chart from the registry mirror over HTTP")
}).WithTimeout(shared.OCIReplicationTimeout).Should(Succeed())

By("verifying ClusterPluginDefinition is Ready")
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cpd), cpd)).To(Succeed())
g.Expect(cpd.Status.IsReadyTrue()).To(BeTrue())
}).WithTimeout(shared.OCIReplicationTimeout).Should(Succeed())

By("verifying chart replication is idempotent on re-reconciliation")
Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cpd), cpd)).To(Succeed())
originalDigest := cpd.GetLastSyncedArtifact().Digest

ann := cpd.GetAnnotations()
if ann == nil {
ann = make(map[string]string)
}
ann[lifecycle.ReconcileAnnotation] = time.Now().UTC().Format(time.RFC3339Nano)
cpd.SetAnnotations(ann)
Expect(adminClient.Update(ctx, cpd)).To(Succeed())

Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cpd), cpd)).To(Succeed())
cond := cpd.Status.StatusConditions.GetConditionByType(greenhousev1alpha1.OCIReplicationReadyCondition)
g.Expect(cond).NotTo(BeNil())
g.Expect(cond.IsTrue()).To(BeTrue())
artifact := cpd.GetLastSyncedArtifact()
g.Expect(artifact).NotTo(BeNil())
g.Expect(artifact.Digest).To(Equal(originalDigest), "re-reconciliation must not change the replicated artifact digest")
}).WithTimeout(shared.OCIReplicationTimeout).Should(Succeed())
}

// ClusterPDChartReplicationFailure verifies that the ClusterPluginDefinition controller sets OCIReplicationFailed for a non-existent chart version.
func ClusterPDChartReplicationFailure(ctx context.Context, adminClient client.Client) {
cpd := test.NewClusterPluginDefinition(ctx, "podinfo-cluster-chart-failure",
test.WithVersion("99.99.99"),
test.WithHelmChart(&greenhousev1alpha1.HelmChartReference{
Name: "podinfo",
Repository: "oci://registry:5000/greenhouse-ghcr-io-mirror/stefanprodan/charts",
Version: "99.99.99",
}),
)

By("creating ClusterPluginDefinition with non-existent chart version")
err := adminClient.Create(ctx, cpd)
Expect(client.IgnoreAlreadyExists(err)).ToNot(HaveOccurred())
DeferCleanup(func() { test.EventuallyDeleted(ctx, adminClient, cpd) })

By("verifying OCIReplicationReady condition becomes False with OCIReplicationFailed reason")
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cpd), cpd)).To(Succeed())
cond := cpd.Status.StatusConditions.GetConditionByType(greenhousev1alpha1.OCIReplicationReadyCondition)
g.Expect(cond).NotTo(BeNil())
g.Expect(cond.IsTrue()).To(BeFalse())
g.Expect(cond.Reason).To(Equal(greenhousev1alpha1.OCIReplicationFailedReason))
}).WithTimeout(shared.OCIReplicationTimeout).Should(Succeed(), "registry mirror pull-through should fail for a non-existent chart version")

By("verifying ClusterPluginDefinition is not Ready")
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cpd), cpd)).To(Succeed())
g.Expect(cpd.Status.IsReadyTrue()).To(BeFalse())
}).WithTimeout(shared.OCIReplicationTimeout).Should(Succeed())
}
11 changes: 11 additions & 0 deletions e2e/clusterplugindefinition/testdata/organization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

apiVersion: greenhouse.sap/v1alpha1
kind: Organization
metadata:
name: greenhouse
spec:
description: greenhouse organization
displayName: Greenhouse
mappedOrgAdminIdPGroup: GREENHOUSE_ORG_ADMIN
1 change: 1 addition & 0 deletions e2e/plugin/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ var _ = BeforeSuite(func() {
Expect(err).ToNot(HaveOccurred(), "there should be no error creating the admin client")
remoteClient, err = clientutil.NewK8sClientFromRestClientGetter(env.RemoteRestClientGetter)
Expect(err).ToNot(HaveOccurred(), "there should be no error creating the remote client")
env = env.WithOrganization(ctx, adminClient, "./testdata/greenhouse_organization.yaml")
env = env.WithOrganization(ctx, adminClient, "./testdata/organization.yaml")
team = test.NewTeam(ctx, "test-plugin-e2e-team", env.TestNamespace, test.WithTeamLabel(greenhouseapis.LabelKeySupportGroup, "true"))
err = adminClient.Create(ctx, team)
Expand Down
12 changes: 12 additions & 0 deletions e2e/plugin/testdata/greenhouse_organization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

# Cluster Scoped Resource
apiVersion: greenhouse.sap/v1alpha1
kind: Organization
metadata:
name: greenhouse
spec:
description: greenhouse organization for cluster-scoped plugin definitions
displayName: Greenhouse
mappedOrgAdminIdPGroup: GREENHOUSE_ORG_ADMIN
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (

type ClusterPluginDefinitionReconciler struct {
client.Client
Scheme *runtime.Scheme
recorder events.EventRecorder
Scheme *runtime.Scheme
recorder events.EventRecorder
OCIMirroringEnabled bool
}

func (r *ClusterPluginDefinitionReconciler) SetupWithManager(name string, mgr ctrl.Manager) error {
Expand All @@ -46,6 +47,9 @@ func (r *ClusterPluginDefinitionReconciler) SetupWithManager(name string, mgr ct
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts/finalizers,verbs=get;create;update;patch;delete
// +kubebuilder:rbac:groups="events.k8s.io",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
// +kubebuilder:rbac:groups=greenhouse.sap,resources=organizations,verbs=get
// +kubebuilder:rbac:groups=greenhouse.sap,resources=clusterplugindefinitions,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=greenhouse.sap,resources=clusterplugindefinitions/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=greenhouse.sap,resources=clusterplugindefinitions/finalizers,verbs=get;create;update;patch;delete
Expand Down Expand Up @@ -81,26 +85,24 @@ func (r *ClusterPluginDefinitionReconciler) EnsureCreated(ctx context.Context, o
recorder: r.recorder,
pluginDef: clusterDef,
namespaceName: flux.HelmRepositoryDefaultNamespace,
ociMirroringEnabled: false,
ociMirroringEnabled: r.OCIMirroringEnabled,
}

helmRepo, err := h.createUpdateHelmRepository(ctx)
if err != nil {
return ctrl.Result{}, lifecycle.Failed, err
}

if replicationErr := h.ensureChartReplication(ctx); replicationErr != nil {
return ctrl.Result{}, lifecycle.Failed, replicationErr
}

helmChart, err := h.createUpdateHelmChart(ctx, helmRepo)
if err != nil {
return ctrl.Result{}, lifecycle.Failed, err
}
h.setHelmChartReadyCondition(ctx, helmChart)

// OCI replication for ClusterPluginDefinitions is not yet supported.
clusterDef.SetCondition(greenhousemetav1alpha1.TrueCondition(
greenhousev1alpha1.OCIReplicationReadyCondition,
greenhousev1alpha1.OCIReplicationNotConfiguredReason,
"OCI replication for ClusterPluginDefinitions is not yet supported"))

return ctrl.Result{}, lifecycle.Success, nil
}

Expand Down
Loading