Skip to content

Commit

Permalink
feat: Add Stage CR namespace validation for multitenancy (#96)
Browse files Browse the repository at this point in the history
This change adds validation for the `Stage.spec.namespace`
field to avoid conflicts when Stage CRs from different
namespaces can point to the same namespace.
Before creating the Stage, the webhook checks namespaces and
returns an error if the namespace(with tenant label)
already exists in the cluster.
zmotso authored and SergK committed Dec 23, 2024

Verified

This commit was signed with the committer’s verified signature. The key has expired.
splattner Sebastian Plattner
1 parent ed5eaa8 commit be6695b
Showing 4 changed files with 86 additions and 2 deletions.
14 changes: 14 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: manager-role
8 changes: 8 additions & 0 deletions deploy-templates/templates/validation_webhook_rbac.yaml
Original file line number Diff line number Diff line change
@@ -13,6 +13,14 @@ rules:
- get
- update
- patch
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch

---

32 changes: 30 additions & 2 deletions pkg/webhook/stage_webhook.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"github.com/epam/edp-cd-pipeline-operator/v2/controllers/stage/chain/util"
corev1 "k8s.io/api/core/v1"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
@@ -16,6 +18,7 @@ import (
const listLimit = 1000

//+kubebuilder:webhook:path=/validate-v2-edp-epam-com-v1-stage,mutating=false,failurePolicy=fail,sideEffects=None,groups=v2.edp.epam.com,resources=stages,verbs=create;update,versions=v1,name=stage.epam.com,admissionReviewVersions=v1
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch

// StageValidationWebhook is a webhook for validating Stage CRD.
type StageValidationWebhook struct {
@@ -49,7 +52,11 @@ func (r *StageValidationWebhook) ValidateCreate(ctx context.Context, obj runtime
return errors.New("the wrong object given, expected Stage")
}

return r.uniqueNamespaces(ctx, createdStage)
if err := r.uniqueTargetNamespaces(ctx, createdStage); err != nil {
return err
}

return r.uniqueTargetNamespaceAcrossCluster(ctx, createdStage)
}

// ValidateUpdate is a webhook for validating the updating of the Stage CR.
@@ -63,7 +70,7 @@ func (*StageValidationWebhook) ValidateDelete(_ context.Context, _ runtime.Objec
return nil
}

func (r *StageValidationWebhook) uniqueNamespaces(ctx context.Context, stage *pipelineApi.Stage) error {
func (r *StageValidationWebhook) uniqueTargetNamespaces(ctx context.Context, stage *pipelineApi.Stage) error {
stages := &pipelineApi.StageList{}

if err := r.client.List(
@@ -92,3 +99,24 @@ func (r *StageValidationWebhook) uniqueNamespaces(ctx context.Context, stage *pi

return nil
}

func (r *StageValidationWebhook) uniqueTargetNamespaceAcrossCluster(ctx context.Context, stage *pipelineApi.Stage) error {
namespaces := &corev1.NamespaceList{}
if err := r.client.List(
ctx,
namespaces,
client.MatchingLabels{
util.TenantLabelName: stage.Spec.Namespace,
},
); err != nil {
return fmt.Errorf("failed to list namespaces: %w", err)
}

for i := range namespaces.Items {
if namespaces.Items[i].Name == stage.Spec.Namespace {
return fmt.Errorf("namespace %s is already used in the cluster", stage.Spec.Namespace)
}
}

return nil
}
34 changes: 34 additions & 0 deletions pkg/webhook/stage_webhook_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ package webhook

import (
"context"
"github.com/epam/edp-cd-pipeline-operator/v2/controllers/stage/chain/util"
corev1 "k8s.io/api/core/v1"
"testing"

"github.com/stretchr/testify/assert"
@@ -20,6 +22,7 @@ func TestStageValidationWebhook_ValidateCreate(t *testing.T) {

scheme := runtime.NewScheme()
require.NoError(t, pipelineApi.AddToScheme(scheme))
require.NoError(t, corev1.AddToScheme(scheme))

tests := []struct {
name string
@@ -112,6 +115,37 @@ func TestStageValidationWebhook_ValidateCreate(t *testing.T) {
require.Contains(t, err.Error(), "namespace stage1-ns is already used in CDPipeline pipeline Stage stage2")
},
},
{
name: "namespace already used in the cluster",
obj: &pipelineApi.Stage{
ObjectMeta: metaV1.ObjectMeta{
Name: "stage1",
Namespace: "default",
},
Spec: pipelineApi.StageSpec{
Name: "stage1",
CdPipeline: "pipeline",
ClusterName: pipelineApi.InCluster,
Namespace: "ns1",
},
},
client: func(t *testing.T) client.Client {
ns1 := &corev1.Namespace{
ObjectMeta: metaV1.ObjectMeta{
Name: "ns1",
Labels: map[string]string{
util.TenantLabelName: "ns1",
},
},
}

return fake.NewClientBuilder().WithScheme(scheme).WithObjects(ns1).Build()
},
wantErr: func(t require.TestingT, err error, i ...interface{}) {
require.Error(t, err)
require.Contains(t, err.Error(), "namespace ns1 is already used in the cluster")
},
},
{
name: "invalid object given",
obj: &codebaseApi.Codebase{},

0 comments on commit be6695b

Please sign in to comment.