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.
  • Loading branch information
zmotso authored and SergK committed Dec 23, 2024
1 parent ed5eaa8 commit be6695b
Show file tree
Hide file tree
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
Expand Down
8 changes: 8 additions & 0 deletions deploy-templates/templates/validation_webhook_rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ rules:
- get
- update
- patch
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch

---

Expand Down
32 changes: 30 additions & 2 deletions pkg/webhook/stage_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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{},
Expand Down

0 comments on commit be6695b

Please sign in to comment.