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
137 changes: 137 additions & 0 deletions e2e/teamrolebinding/e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

//go:build teamrolebindingE2E

package teamrolebinding

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"

greenhouseapis "github.com/cloudoperators/greenhouse/api"
greenhousev1alpha1 "github.com/cloudoperators/greenhouse/api/v1alpha1"
"github.com/cloudoperators/greenhouse/e2e/shared"
"github.com/cloudoperators/greenhouse/e2e/teamrolebinding/scenarios"
"github.com/cloudoperators/greenhouse/internal/clientutil"
"github.com/cloudoperators/greenhouse/internal/test"
)

const (
remoteClusterName = "remote-trb-cluster"
)

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

teamAlpha *greenhousev1alpha1.Team
teamBeta *greenhousev1alpha1.Team
teamRole *greenhousev1alpha1.TeamRole

trb scenarios.ITRBScenario
)

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

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

var err error
env = shared.NewExecutionEnv()
adminClient, err = clientutil.NewK8sClientFromRestClientGetter(env.AdminRestClientGetter)
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/organization.yaml")

Comment thread
Zaggy21 marked this conversation as resolved.
By("creating shared Teams")
teamAlpha = test.NewTeam(ctx, "trb-team-alpha", env.TestNamespace,
test.WithMappedIDPGroup("idp-group-trb-alpha"),
test.WithTeamLabel(greenhouseapis.LabelKeySupportGroup, "true"),
)
err = adminClient.Create(ctx, teamAlpha)
Expect(client.IgnoreAlreadyExists(err)).ToNot(HaveOccurred(), "there should be no error creating teamAlpha")

teamBeta = test.NewTeam(ctx, "trb-team-beta", env.TestNamespace,
test.WithMappedIDPGroup("idp-group-trb-beta"),
test.WithTeamLabel(greenhouseapis.LabelKeySupportGroup, "true"),
)
err = adminClient.Create(ctx, teamBeta)
Expect(client.IgnoreAlreadyExists(err)).ToNot(HaveOccurred(), "there should be no error creating teamBeta")

By("onboarding the remote cluster")
shared.OnboardRemoteCluster(ctx, adminClient, env.RemoteKubeConfigBytes, remoteClusterName, env.TestNamespace, teamAlpha.Name)
shared.ClusterIsReady(ctx, adminClient, remoteClusterName, env.TestNamespace)

By("creating shared TeamRole")
teamRole = test.NewTeamRole(ctx, "trb-test-role", env.TestNamespace)
err = adminClient.Create(ctx, teamRole)
Expect(client.IgnoreAlreadyExists(err)).ToNot(HaveOccurred(), "there should be no error creating teamRole")

By("building scenario runner")
trb = scenarios.NewScenario(
adminClient, remoteClient,
env.TestNamespace, remoteClusterName,
teamAlpha, teamBeta,
teamRole,
)

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

var _ = AfterSuite(func() {
By("deleting shared resources")
test.EventuallyDeleted(ctx, adminClient, teamRole)
test.EventuallyDeleted(ctx, adminClient, teamAlpha)
test.EventuallyDeleted(ctx, adminClient, teamBeta)

By("off-boarding the remote cluster")
shared.OffBoardRemoteCluster(ctx, adminClient, remoteClient, testStartTime, remoteClusterName, env.TestNamespace)

env.GenerateGreenhouseControllerLogs(ctx, testStartTime)
})

var _ = Describe("TeamRoleBinding E2E", Ordered, func() {
DescribeTable("TeamRoleBinding scenarios",
func(execute func(scenarios.ITRBScenario, context.Context)) {
execute(trb, ctx)
},
Entry("Single teamRef (baseline)",
func(s scenarios.ITRBScenario, c context.Context) { s.ExecuteSingleTeamRefScenario(c) },
),
Entry("Multiple teamRefs",
func(s scenarios.ITRBScenario, c context.Context) { s.ExecuteMultipleTeamRefsScenario(c) },
),
Entry("Deprecated teamRef migration",
func(s scenarios.ITRBScenario, c context.Context) { s.ExecuteDeprecatedTeamRefMigrationScenario(c) },
),
Entry("Mutation of teamRefs (add/remove)",
func(s scenarios.ITRBScenario, c context.Context) { s.ExecuteTeamRefsMutationScenario(c) },
),
Entry("Partial failure (some teams missing)",
func(s scenarios.ITRBScenario, c context.Context) { s.ExecutePartialFailureScenario(c) },
),
Entry("Namespace creation with multiple teams",
func(s scenarios.ITRBScenario, c context.Context) { s.ExecuteNamespaceCreationScenario(c) },
),
Entry("Cluster selector",
func(s scenarios.ITRBScenario, c context.Context) { s.ExecuteClusterSelectorScenario(c) },
),
)
})
86 changes: 86 additions & 0 deletions e2e/teamrolebinding/scenarios/scenario.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package scenarios

import (
"context"

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

greenhousev1alpha1 "github.com/cloudoperators/greenhouse/api/v1alpha1"
greenhousev1alpha2 "github.com/cloudoperators/greenhouse/api/v1alpha2"
"github.com/cloudoperators/greenhouse/internal/clientutil"
"github.com/cloudoperators/greenhouse/internal/test"
)

// ITRBScenario defines all executable TeamRoleBinding E2E scenarios.
type ITRBScenario interface {
// ExecuteSingleTeamRefScenario verifies RBAC for a single teamRefs entry (baseline).
ExecuteSingleTeamRefScenario(ctx context.Context)
// ExecuteMultipleTeamRefsScenario verifies RBAC for two teams in teamRefs.
ExecuteMultipleTeamRefsScenario(ctx context.Context)
// ExecuteDeprecatedTeamRefMigrationScenario verifies the webhook migrates teamRef β†’ teamRefs.
ExecuteDeprecatedTeamRefMigrationScenario(ctx context.Context)
// ExecuteTeamRefsMutationScenario verifies subjects update when teamRefs are modified.
ExecuteTeamRefsMutationScenario(ctx context.Context)
// ExecutePartialFailureScenario verifies RBAC resilience when some teams are missing.
ExecutePartialFailureScenario(ctx context.Context)
// ExecuteNamespaceCreationScenario verifies createNamespaces=true with multiple teams.
ExecuteNamespaceCreationScenario(ctx context.Context)
// ExecuteClusterSelectorScenario verifies RBAC is applied only to clusters matching a selector.
ExecuteClusterSelectorScenario(ctx context.Context)
}

// scenario holds the shared state for all TeamRoleBinding E2E scenarios.
type scenario struct {
adminClient client.Client
remoteClient client.Client
namespace string
clusterName string
teamAlpha *greenhousev1alpha1.Team
teamBeta *greenhousev1alpha1.Team
teamRole *greenhousev1alpha1.TeamRole
}

// NewScenario constructs an ITRBScenario from the given test context.
func NewScenario(
adminClient, remoteClient client.Client,
namespace, clusterName string,
teamAlpha, teamBeta *greenhousev1alpha1.Team,
teamRole *greenhousev1alpha1.TeamRole,
) ITRBScenario {

GinkgoHelper()
return &scenario{
adminClient: adminClient,
remoteClient: remoteClient,
namespace: namespace,
clusterName: clusterName,
teamAlpha: teamAlpha,
teamBeta: teamBeta,
teamRole: teamRole,
}
}

// createTRB is a helper that creates or patches a TeamRoleBinding and returns it.
func (s *scenario) createTRB(ctx context.Context, name string, opts ...func(*greenhousev1alpha2.TeamRoleBinding)) *greenhousev1alpha2.TeamRoleBinding {
GinkgoHelper()
trb := test.NewTeamRoleBinding(ctx, name, s.namespace)
_, err := clientutil.CreateOrPatch(ctx, s.adminClient, trb, func() error {
for _, opt := range opts {
opt(trb)
}
return nil
Comment thread
Zaggy21 marked this conversation as resolved.
})
Expect(err).ToNot(HaveOccurred(), "there should be no error creating or patching %T %s", trb, trb.GetName())
return trb
}

// cleanup deletes a TeamRoleBinding if it is non-nil, waiting for it to disappear.
func (s *scenario) cleanup(ctx context.Context, trb *greenhousev1alpha2.TeamRoleBinding) {
GinkgoHelper()
test.EventuallyDeleted(ctx, s.adminClient, trb)
}
104 changes: 104 additions & 0 deletions e2e/teamrolebinding/scenarios/trb_cluster_selector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package scenarios

import (
"context"
"slices"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
"sigs.k8s.io/controller-runtime/pkg/client"

greenhousev1alpha1 "github.com/cloudoperators/greenhouse/api/v1alpha1"
greenhousev1alpha2 "github.com/cloudoperators/greenhouse/api/v1alpha2"
"github.com/cloudoperators/greenhouse/internal/clientutil"
"github.com/cloudoperators/greenhouse/internal/test"
)

// ExecuteClusterSelectorScenario verifies that a TeamRoleBinding with a label-based
// clusterSelector applies RBAC only to clusters whose labels match the selector, and that RBAC
// is removed from a cluster when it no longer matches (label change).
func (s *scenario) ExecuteClusterSelectorScenario(ctx context.Context) {
GinkgoHelper()
var trb *greenhousev1alpha2.TeamRoleBinding

// A unique label key used only by this test run to avoid interference.
labelKey := "trb-selector-test-" + rand.String(6)
const labelMatch = "match"
const labelNoMatch = "no-match"
Comment thread
Zaggy21 marked this conversation as resolved.

By("adding a unique label to the remote cluster for the selector test")
cluster := &greenhousev1alpha1.Cluster{}
Expect(s.adminClient.Get(ctx, client.ObjectKey{Name: s.clusterName, Namespace: s.namespace}, cluster)).
To(Succeed(), "remote cluster should exist")

_, err := clientutil.CreateOrPatch(ctx, s.adminClient, cluster, func() error {
if cluster.Labels == nil {
cluster.Labels = make(map[string]string)
}
cluster.Labels[labelKey] = labelMatch
return nil
})
Expect(err).ToNot(HaveOccurred(), "there should be no error labeling the cluster")

// Restore the cluster label on cleanup regardless of test outcome.
DeferCleanup(func() {
_, err := clientutil.CreateOrPatch(ctx, s.adminClient, cluster, func() error {
delete(cluster.Labels, labelKey)
return nil
})
Expect(err).ToNot(HaveOccurred(), "there should be no error restoring the cluster label during cleanup")
s.cleanup(ctx, trb)
})

By("creating a TeamRoleBinding with a label selector matching the remote cluster")
trb = s.createTRB(ctx, "trb-selector",
test.WithTeamRoleRef(s.teamRole.Name),
test.WithTeamRefs(s.teamAlpha.Name, s.teamBeta.Name),
test.WithClusterSelector(metav1.LabelSelector{
MatchLabels: map[string]string{labelKey: labelMatch},
}),
)

By("verifying the ClusterRoleBinding is created on the matching remote cluster with subjects for both teams")
remoteCRB := &rbacv1.ClusterRoleBinding{}
Eventually(func(g Gomega) {
g.Expect(s.remoteClient.Get(ctx, client.ObjectKey{Name: trb.GetRBACName()}, remoteCRB)).
To(Succeed(), "ClusterRoleBinding should be created on the matching cluster")
g.Expect(slices.ContainsFunc(remoteCRB.Subjects, func(sub rbacv1.Subject) bool {
return sub.Kind == rbacv1.GroupKind && sub.Name == s.teamAlpha.Spec.MappedIDPGroup
})).To(BeTrue(), "subjects should contain teamAlpha's IDP group")
g.Expect(slices.ContainsFunc(remoteCRB.Subjects, func(sub rbacv1.Subject) bool {
return sub.Kind == rbacv1.GroupKind && sub.Name == s.teamBeta.Spec.MappedIDPGroup
})).To(BeTrue(), "subjects should contain teamBeta's IDP group")
}).Should(Succeed(), "ClusterRoleBinding should exist on the matching cluster with subjects for both teams")

By("verifying the TeamRoleBinding PropagationStatus references only the matching cluster")
Eventually(func(g Gomega) {
g.Expect(s.adminClient.Get(ctx, client.ObjectKeyFromObject(trb), trb)).To(Succeed())
g.Expect(trb.Status.PropagationStatus).To(HaveLen(1),
"only one cluster should appear in PropagationStatus")
g.Expect(trb.Status.PropagationStatus[0].ClusterName).To(Equal(s.clusterName))
g.Expect(trb.Status.PropagationStatus[0].Condition.Status).To(Equal(metav1.ConditionTrue))
}).Should(Succeed(), "PropagationStatus should only show the matching cluster")

By("changing the cluster label so it no longer matches the selector")
_, err = clientutil.CreateOrPatch(ctx, s.adminClient, cluster, func() error {
cluster.Labels[labelKey] = labelNoMatch
return nil
})
Expect(err).ToNot(HaveOccurred(), "there should be no error updating cluster label")

By("verifying the ClusterRoleBinding is removed after the cluster label no longer matches")
Eventually(func(g Gomega) {
err := s.remoteClient.Get(ctx, client.ObjectKey{Name: trb.GetRBACName()}, remoteCRB)
g.Expect(apierrors.IsNotFound(err)).To(BeTrue(),
"ClusterRoleBinding should be removed after cluster label no longer matches the selector")
}).Should(Succeed(), "ClusterRoleBinding should be cleaned up when cluster no longer matches selector")
}
58 changes: 58 additions & 0 deletions e2e/teamrolebinding/scenarios/trb_deprecated_migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package scenarios

import (
"context"
"slices"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

greenhousev1alpha2 "github.com/cloudoperators/greenhouse/api/v1alpha2"
"github.com/cloudoperators/greenhouse/internal/test"
)

// ExecuteDeprecatedTeamRefMigrationScenario verifies that the mutating webhook migrates the
// deprecated singular teamRef field into teamRefs, and that RBAC is applied correctly.
func (s *scenario) ExecuteDeprecatedTeamRefMigrationScenario(ctx context.Context) {
GinkgoHelper()
var trb *greenhousev1alpha2.TeamRoleBinding
DeferCleanup(func() { s.cleanup(ctx, trb) })

By("creating a TeamRoleBinding using the deprecated teamRef field")
trb = s.createTRB(ctx, "trb-deprecated",
test.WithTeamRoleRef(s.teamRole.Name),
test.WithTeamRef(s.teamAlpha.Name),
test.WithClusterName(s.clusterName),
)

By("verifying the webhook migrated teamRef into teamRefs")
Eventually(func(g Gomega) {
g.Expect(s.adminClient.Get(ctx, client.ObjectKeyFromObject(trb), trb)).To(Succeed())
g.Expect(trb.Spec.TeamRefs).To(ContainElement(s.teamAlpha.Name),
"teamRefs should contain the migrated teamRef value")
}).Should(Succeed(), "teamRef should be migrated into teamRefs by the webhook")

By("verifying the ClusterRoleBinding is created on the remote cluster with the correct subject")
remoteCRB := &rbacv1.ClusterRoleBinding{}
Eventually(func(g Gomega) {
g.Expect(s.remoteClient.Get(ctx, client.ObjectKey{Name: trb.GetRBACName()}, remoteCRB)).
To(Succeed(), "ClusterRoleBinding should exist on the remote cluster")
g.Expect(slices.ContainsFunc(remoteCRB.Subjects, func(sub rbacv1.Subject) bool {
return sub.Kind == rbacv1.GroupKind && sub.Name == s.teamAlpha.Spec.MappedIDPGroup
})).To(BeTrue(), "subjects should contain teamAlpha's IDP group after migration")
}).Should(Succeed(), "ClusterRoleBinding should be created with the correct subject after teamRef migration")

By("verifying the TeamRoleBinding RBACReady status is True")
Eventually(func(g Gomega) {
g.Expect(s.adminClient.Get(ctx, client.ObjectKeyFromObject(trb), trb)).To(Succeed())
cond := trb.Status.GetConditionByType(greenhousev1alpha2.RBACReady)
g.Expect(cond).NotTo(BeNil())
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
}).Should(Succeed(), "RBACReady should be True after deprecated teamRef migration")
}
Loading
Loading