Repository: metallb/metallb
Pull request #1523 (2022-07-19 11:19:23)

added namespace validation for custom resources
Fixed https://github.com/metallb/metallb/issues/1517 


mgsh: Hi @fedepaol , I've attempted a fix for namespace validation. Can you please review when you get a chance and suggest if any changes?
fedepaol: > Hi @fedepaol , I've attempted a fix for namespace validation. Can you please review when you get a chance and suggest if any changes?

sure thing, and thanks for the contribution! :heart: 
fedepaol: Looks good! A couple of quick nits:

- Can you change the commit message according to https://chris.beams.io/posts/git-commit/
- Can you add an e2e test under https://github.com/metallb/metallb/blob/main/e2etest/webhookstests/webhooks.go ?
- I will ask you to add it to the release notes, but it will probably end up in a latter release so adding to 0.13.4 doesn't make sense now. Let's see if we are able to cut a release today
fedepaol: > Sure @fedepaol. I'll try and resolve your changes by today.

no rush!
mgsh: Just an update on status:
It took me some time to understand ginkgo setup and testing. I have created test cases and it's working as expected for all CRs except BFDProfiles which is not failing as expected when tested using the e2etests. I'm still debugging. Should be done by tomorrow.
fedepaol: > Just an update on status: It took me some time to understand ginkgo setup and testing. I have created test cases and it's working as expected for all CRs except BFDProfiles which is not failing as expected when tested using the e2etests. I'm still debugging. Should be done by tomorrow.

Sure thing! Again, no rush :-)
mgsh: @fedepaol, I've added e2tests and pushed another commit (BFDProfile test was failing earlier because its webhook config did NOT have a CREATE hook registered, I've added it now, hope that's fine).

As for clear commit message, I currently have 2 separate commits which I've pushed already. Would you recommend that I rebase/squash both commits into one single commit and force push in its current branch (I was reluctant to try it without checking first, because it's usually not advised)? OR perhaps a new PR with just the changes applied as a single commit with a clear commit message? Please advise.

As for the message content, I think your description on the issue was sufficiently verbose. Based on your advise on rebase/squash or squash/merge we can use the below message accordingly (with your changes if any).
```
Added namespace validation in the create webhook for all CRs

Currently, there's nothing preventing the CRs to be created in a
namespace different from the one metallb is deployed to. This change
adds namespace validation to prevent creation of CRs in a different
namespace.
```
fedepaol: > @fedepaol, I've added e2tests and pushed another commit (BFDProfile test was failing earlier because its webhook config did NOT have a CREATE hook registered, I've added it now, hope that's fine).
> 
> As for clear commit message, I currently have 2 separate commits which I've pushed already. Would you recommend that I rebase/squash both commits into one single commit and force push in its current branch (I was reluctant to try it without checking first, because it's usually not advised)? OR perhaps a new PR with just the changes applied as a single commit with a clear commit message? Please advise.
> 
> As for the message content, I think your description on the issue was sufficiently verbose. Based on your advise on rebase/squash or squash/merge we can use the below message accordingly (with your changes if any).
> 
> ```
> Added namespace validation in the create webhook for all CRs
> 
> Currently, there's nothing preventing the CRs to be created in a
> namespace different from the one metallb is deployed to. This change
> adds namespace validation to prevent creation of CRs in a different
> namespace.
> ```

Commit message is fine. We normally split the changes with a commit for the changes and another one for the tests.
Rebasing is fine. 
I'd also add a section to the release notes (you can create v0.13.5) in a separate commit to mention this change.
mgsh: Let me check the e2e failures and fix, certain scenarios are failing.
mgsh: _My bad that I hadn't run all test possibilities (am exploring the use of `act` to run github actions locally before pushing changes, yet to get it fully working)._

The helm based e2etests were failing. I had to make 2 changes:
- Add `CREATE` op in the helm chart bfdprofile validation webhook config (and also fixed a possible typo) https://github.com/metallb/metallb/pull/1523/commits/163cf03ac1941ef844e3bd50faa8ea935019a756#diff-dbf01c8949b94f349b1a6e20f162b6cacb22d3348e400200384e29100b990669
- Updated the e2etest setup to use `kube-system` as the other namespace to expect failures in CR creation. I had used `default` earlier, but the helm based tests are using the `default` namespace to install metallb. Is this expected, shouldn't we be testing with `helm install --create-namespace --namespace metallb-system ...`?
mgsh: The operator based e2etest required a change in the metallb-operator repo (to enable validation webhook for BFDProfile create op). Tracking that change under https://github.com/metallb/metallb-operator/pull/240 (from what I understand, it fetches configs from this repo and that's causing its manifest validation tests to fail, looking into it).
fedepaol: >     * Updated the e2etest setup to use `kube-system` as the other namespace to expect failures in CR creation. I had used `default` earlier, but the helm based tests are using the `default` namespace to install metallb. Is this expected, shouldn't we be testing with `helm install --create-namespace --namespace metallb-system ...`?

Yep, this is something that we can definitely improve. Regarding the 'other namespace', why not creating one? Despite it's reasonable to expect `kube-system` , maybe it makes sense to be more in control with that (deleting the namespace when the suite exits)
mgsh: I've made these two changes:
1. create a different namespace for the namespace validation tests.
2. use the fork version of metallb-operator (needs to be changed back to main once that fork is merged).
fedepaol: >     2. use the fork version of metallb-operator (needs to be changed back to main once that fork is merged).

when we deal with this chicken-egg situations, it's fine to have the operator lane temporary failing. I'd go that way, to avoid the need of reverting again.
mgsh: > when we deal with this chicken-egg situations, it's fine to have the operator lane temporary failing. I'd go that way, to avoid the need of reverting again.

Got it. In that case, let me remove that commit (https://github.com/metallb/metallb/pull/1523/commits/6c78ab3bbeb623855fc67c8dcb287036328b3923) and push it again. I've tested the above commit already for all tests passing in my forked repo (https://github.com/mgsh/metallb/pull/1), to cross-check that it was the only missing dependency. So once the metallb-operator changes are sorted out e2e-use-operator tests will be automatically fixed.
fedepaol: Couple of small nits, and I think this is good to land!
fedepaol: LGTM, thanks!


Patch:
diff --git a/api/v1beta1/addresspool_webhook.go b/api/v1beta1/addresspool_webhook.go
index 17e2eb1e..58432315 100644
--- a/api/v1beta1/addresspool_webhook.go
+++ b/api/v1beta1/addresspool_webhook.go
@@ -18,6 +18,7 @@ package v1beta1
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/go-kit/log/level"
 	"github.com/pkg/errors"
@@ -41,6 +42,10 @@ var _ webhook.Validator = &AddressPool{}
 func (addressPool *AddressPool) ValidateCreate() error {
 	level.Debug(Logger).Log("webhook", "addressPool", "action", "create", "name", addressPool.Name, "namespace", addressPool.Namespace)
 
+	if addressPool.Namespace != MetalLBNamespace {
+		return fmt.Errorf("resource must be created in %s namespace", MetalLBNamespace)
+	}
+
 	existingAddressPoolList, err := getExistingAddressPools()
 	if err != nil {
 		return err
diff --git a/api/v1beta1/bfdprofile_webhook.go b/api/v1beta1/bfdprofile_webhook.go
index e4b7f505..2d8cbe51 100644
--- a/api/v1beta1/bfdprofile_webhook.go
+++ b/api/v1beta1/bfdprofile_webhook.go
@@ -32,12 +32,18 @@ func (bfdProfile *BFDProfile) SetupWebhookWithManager(mgr ctrl.Manager) error {
 		Complete()
 }
 
-//+kubebuilder:webhook:verbs=delete,path=/validate-metallb-io-v1beta1-bfdprofile,mutating=false,failurePolicy=fail,groups=metallb.io,resources=bfdprofiles,versions=v1beta1,name=bfdprofilevalidationwebhook.metallb.io,sideEffects=None,admissionReviewVersions=v1
+//+kubebuilder:webhook:verbs=create;delete,path=/validate-metallb-io-v1beta1-bfdprofile,mutating=false,failurePolicy=fail,groups=metallb.io,resources=bfdprofiles,versions=v1beta1,name=bfdprofilevalidationwebhook.metallb.io,sideEffects=None,admissionReviewVersions=v1
 
 var _ webhook.Validator = &BFDProfile{}
 
 // ValidateCreate implements webhook.Validator so a webhook will be registered for BFDProfile.
 func (bfdProfile *BFDProfile) ValidateCreate() error {
+	level.Debug(Logger).Log("webhook", "bfdProfile", "action", "create", "name", bfdProfile.Name, "namespace", bfdProfile.Namespace)
+
+	if bfdProfile.Namespace != MetalLBNamespace {
+		return fmt.Errorf("resource must be created in %s namespace", MetalLBNamespace)
+	}
+
 	return nil
 }
 
diff --git a/api/v1beta1/bgpadvertisement_webhook.go b/api/v1beta1/bgpadvertisement_webhook.go
index 4cfe46d9..c07fe864 100644
--- a/api/v1beta1/bgpadvertisement_webhook.go
+++ b/api/v1beta1/bgpadvertisement_webhook.go
@@ -18,6 +18,7 @@ package v1beta1
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/go-kit/log/level"
 	"github.com/pkg/errors"
@@ -41,6 +42,10 @@ var _ webhook.Validator = &BGPAdvertisement{}
 func (bgpAdv *BGPAdvertisement) ValidateCreate() error {
 	level.Debug(Logger).Log("webhook", "bgpadvertisement", "action", "create", "name", bgpAdv.Name, "namespace", bgpAdv.Namespace)
 
+	if bgpAdv.Namespace != MetalLBNamespace {
+		return fmt.Errorf("resource must be created in %s namespace", MetalLBNamespace)
+	}
+
 	existingBGPAdvList, err := getExistingBGPAdvs()
 	if err != nil {
 		return err
diff --git a/api/v1beta1/community_webhook.go b/api/v1beta1/community_webhook.go
index 8b19a9b4..cde10f5c 100644
--- a/api/v1beta1/community_webhook.go
+++ b/api/v1beta1/community_webhook.go
@@ -18,6 +18,7 @@ package v1beta1
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/go-kit/log/level"
 	"github.com/pkg/errors"
@@ -41,6 +42,10 @@ var _ webhook.Validator = &Community{}
 func (community *Community) ValidateCreate() error {
 	level.Debug(Logger).Log("webhook", "community", "action", "create", "name", community.Name, "namespace", community.Namespace)
 
+	if community.Namespace != MetalLBNamespace {
+		return fmt.Errorf("resource must be created in %s namespace", MetalLBNamespace)
+	}
+
 	existingCommunityList, err := getExistingCommunities()
 	if err != nil {
 		return err
diff --git a/api/v1beta1/ipaddresspool_webhook.go b/api/v1beta1/ipaddresspool_webhook.go
index e80b57f2..23b5d486 100644
--- a/api/v1beta1/ipaddresspool_webhook.go
+++ b/api/v1beta1/ipaddresspool_webhook.go
@@ -18,6 +18,7 @@ package v1beta1
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/go-kit/log/level"
 	"github.com/pkg/errors"
@@ -41,6 +42,9 @@ var _ webhook.Validator = &IPAddressPool{}
 func (ipAddress *IPAddressPool) ValidateCreate() error {
 	level.Debug(Logger).Log("webhook", "ipaddresspool", "action", "create", "name", ipAddress.Name, "namespace", ipAddress.Namespace)
 
+	if ipAddress.Namespace != MetalLBNamespace {
+		return fmt.Errorf("resource must be created in %s namespace", MetalLBNamespace)
+	}
 	existingIPAddressPoolList, err := getExistingIPAddressPools()
 	if err != nil {
 		return err
diff --git a/api/v1beta1/l2advertisement_webhook.go b/api/v1beta1/l2advertisement_webhook.go
index 29e30296..cc2f6e46 100644
--- a/api/v1beta1/l2advertisement_webhook.go
+++ b/api/v1beta1/l2advertisement_webhook.go
@@ -18,6 +18,7 @@ package v1beta1
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/go-kit/log/level"
 	"github.com/pkg/errors"
@@ -41,6 +42,10 @@ var _ webhook.Validator = &L2Advertisement{}
 func (l2Adv *L2Advertisement) ValidateCreate() error {
 	level.Debug(Logger).Log("webhook", "l2advertisement", "action", "create", "name", l2Adv.Name, "namespace", l2Adv.Namespace)
 
+	if l2Adv.Namespace != MetalLBNamespace {
+		return fmt.Errorf("resource must be created in %s namespace", MetalLBNamespace)
+	}
+
 	existingL2AdvList, err := getExistingL2Advs()
 	if err != nil {
 		return err
diff --git a/api/v1beta2/bgppeer_webhook.go b/api/v1beta2/bgppeer_webhook.go
index a8cbcf3f..2a2a08fe 100644
--- a/api/v1beta2/bgppeer_webhook.go
+++ b/api/v1beta2/bgppeer_webhook.go
@@ -18,6 +18,7 @@ package v1beta2
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/go-kit/log/level"
 	"github.com/pkg/errors"
@@ -41,6 +42,9 @@ var _ webhook.Validator = &BGPPeer{}
 func (bgpPeer *BGPPeer) ValidateCreate() error {
 	level.Debug(Logger).Log("webhook", "bgppeer", "action", "create", "name", bgpPeer.Name, "namespace", bgpPeer.Namespace)
 
+	if bgpPeer.Namespace != MetalLBNamespace {
+		return fmt.Errorf("resource must be created in %s namespace", MetalLBNamespace)
+	}
 	existingBGPPeers, err := GetExistingBGPPeers()
 	if err != nil {
 		return err
diff --git a/charts/metallb/templates/webhooks.yaml b/charts/metallb/templates/webhooks.yaml
index 34c0429e..22376522 100644
--- a/charts/metallb/templates/webhooks.yaml
+++ b/charts/metallb/templates/webhooks.yaml
@@ -113,13 +113,14 @@ webhooks:
       namespace: {{ .Release.Namespace }}
       path: /validate-metallb-io-v1beta1-bfdprofile
   failurePolicy: Fail
-  name: bfdprofileyvalidationwebhook.metallb.io
+  name: bfdprofilevalidationwebhook.metallb.io
   rules:
   - apiGroups:
     - metallb.io
     apiVersions:
     - v1beta1
     operations:
+    - CREATE
     - DELETE
     resources:
     - bfdprofiles
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index f538f4fd..c09d37a1 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -1999,6 +1999,7 @@ webhooks:
     apiVersions:
     - v1beta1
     operations:
+    - CREATE
     - DELETE
     resources:
     - bfdprofiles
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index 83912e6e..72d1c772 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -1789,6 +1789,7 @@ webhooks:
     apiVersions:
     - v1beta1
     operations:
+    - CREATE
     - DELETE
     resources:
     - bfdprofiles
diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index 301a7d7e..ff84ddef 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -61,6 +61,7 @@ webhooks:
     apiVersions:
     - v1beta1
     operations:
+    - CREATE
     - DELETE
     resources:
     - bfdprofiles
diff --git a/website/content/release-notes/_index.md b/website/content/release-notes/_index.md
index c00fead8..79ad5ae9 100644
--- a/website/content/release-notes/_index.md
+++ b/website/content/release-notes/_index.md
@@ -2,6 +2,11 @@
 title: Release Notes
 weight: 8
 ---
+## Version 0.13.5
+
+New Features:
+
+- Added namespace validation for custom resources ([PR 1523](https://github.com/metallb/metallb/pull/1523))
 
 ## Version 0.13.4
 


Test patch:
diff --git a/api/v1beta1/addresspool_webhook_test.go b/api/v1beta1/addresspool_webhook_test.go
index 453f9744..0d0f2e8a 100644
--- a/api/v1beta1/addresspool_webhook_test.go
+++ b/api/v1beta1/addresspool_webhook_test.go
@@ -15,6 +15,7 @@ const (
 )
 
 func TestValidateAddressPool(t *testing.T) {
+	MetalLBNamespace = MetalLBTestNameSpace
 	addressPool := AddressPool{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "test-addresspool",
@@ -115,6 +116,18 @@ func TestValidateAddressPool(t *testing.T) {
 			},
 			failValidate: true,
 		},
+		{
+			desc: "Validation must fail if created in different namespace",
+			addressPool: &AddressPool{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-addresspool2",
+					Namespace: "default",
+				},
+			},
+			isNewAddressPool: true,
+			expected:         nil,
+			failValidate:     true,
+		},
 	}
 
 	for _, test := range tests {
diff --git a/api/v1beta1/bfdprofile_webhook_test.go b/api/v1beta1/bfdprofile_webhook_test.go
index df7b25e5..f27a4b6f 100644
--- a/api/v1beta1/bfdprofile_webhook_test.go
+++ b/api/v1beta1/bfdprofile_webhook_test.go
@@ -5,12 +5,15 @@ package v1beta1
 import (
 	"testing"
 
+	"github.com/go-kit/log"
 	"go.universe.tf/metallb/api/v1beta2"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func TestValidateBFDProfile(t *testing.T) {
+	MetalLBNamespace = MetalLBTestNameSpace
+	Logger = log.NewNopLogger()
 	toRestoreBGPPeers := v1beta2.GetExistingBGPPeers
 	v1beta2.GetExistingBGPPeers = func() (*v1beta2.BGPPeerList, error) {
 		return &v1beta2.BGPPeerList{
@@ -31,17 +34,51 @@ func TestValidateBFDProfile(t *testing.T) {
 		v1beta2.GetExistingBGPPeers = toRestoreBGPPeers
 	}()
 
-	desc := "Delete bfdprofile used by bgppeer"
-	bfdProfile := &BFDProfile{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "bfdprofile",
-			Namespace: MetalLBTestNameSpace,
+	const (
+		isNew int = iota
+		isDel
+	)
+	tests := []struct {
+		desc         string
+		bfdProfile   *BFDProfile
+		validateType int
+		failValidate bool
+	}{
+		{
+			desc:         "Delete bfdprofile used by bgppeer",
+			validateType: isDel,
+			bfdProfile: &BFDProfile{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "bfdprofile",
+					Namespace: MetalLBTestNameSpace,
+				},
+			},
+			failValidate: true,
+		},
+		{
+			desc:         "Validation must fail if created in different namespace",
+			validateType: isNew,
+			bfdProfile: &BFDProfile{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "bfdprofile1",
+					Namespace: "default",
+				},
+			},
+			failValidate: true,
 		},
 	}
 
-	err := bfdProfile.ValidateDelete()
+	for _, test := range tests {
+		var err error
+		switch test.validateType {
+		case isNew:
+			err = test.bfdProfile.ValidateCreate()
+		case isDel:
+			err = test.bfdProfile.ValidateDelete()
+		}
 
-	if err == nil {
-		t.Fatalf("test %s failed, expecting error", desc)
+		if test.failValidate && err == nil {
+			t.Fatalf("test %s failed, expecting error", test.desc)
+		}
 	}
 }
diff --git a/api/v1beta1/bgpadvertisement_webhook_test.go b/api/v1beta1/bgpadvertisement_webhook_test.go
index 64a9b56b..b3402710 100644
--- a/api/v1beta1/bgpadvertisement_webhook_test.go
+++ b/api/v1beta1/bgpadvertisement_webhook_test.go
@@ -11,6 +11,7 @@ import (
 )
 
 func TestValidateBGPAdvertisement(t *testing.T) {
+	MetalLBNamespace = MetalLBTestNameSpace
 	bgpAdv := BGPAdvertisement{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "test-bgpadv",
@@ -117,6 +118,18 @@ func TestValidateBGPAdvertisement(t *testing.T) {
 			},
 			failValidate: true,
 		},
+		{
+			desc: "Validation must fail if created in different namespace",
+			bgpAdv: &BGPAdvertisement{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-bgpadv1",
+					Namespace: "default",
+				},
+			},
+			isNew:        true,
+			expected:     nil,
+			failValidate: true,
+		},
 	}
 	for _, test := range tests {
 		var err error
diff --git a/api/v1beta1/community_webhook_test.go b/api/v1beta1/community_webhook_test.go
index bcdd9fd1..3f4f7fda 100644
--- a/api/v1beta1/community_webhook_test.go
+++ b/api/v1beta1/community_webhook_test.go
@@ -11,6 +11,7 @@ import (
 )
 
 func TestValidateCommunity(t *testing.T) {
+	MetalLBNamespace = MetalLBTestNameSpace
 	Logger = log.NewNopLogger()
 
 	toRestoreCommunities := getExistingCommunities
@@ -104,6 +105,18 @@ func TestValidateCommunity(t *testing.T) {
 			},
 			failValidate: true,
 		},
+		{
+			desc: "Validation must fail if created in different namespace",
+			commuinty: &Community{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-commuinty2",
+					Namespace: "default",
+				},
+			},
+			isNewCommunity: true,
+			expected:       nil,
+			failValidate:   true,
+		},
 	}
 	for _, test := range tests {
 		var err error
diff --git a/api/v1beta1/ipaddresspool_webhook_test.go b/api/v1beta1/ipaddresspool_webhook_test.go
index 70585815..1170f28d 100644
--- a/api/v1beta1/ipaddresspool_webhook_test.go
+++ b/api/v1beta1/ipaddresspool_webhook_test.go
@@ -11,6 +11,7 @@ import (
 )
 
 func TestValidateIPAddressPool(t *testing.T) {
+	MetalLBNamespace = MetalLBTestNameSpace
 	ipAddressPool := IPAddressPool{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "test-ippool",
@@ -111,6 +112,18 @@ func TestValidateIPAddressPool(t *testing.T) {
 			},
 			failValidate: true,
 		},
+		{
+			desc: "Validation must fail if created in different namespace",
+			ipAddressPool: &IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ippool2",
+					Namespace: "default",
+				},
+			},
+			isNew:        true,
+			expected:     nil,
+			failValidate: true,
+		},
 	}
 
 	for _, test := range tests {
diff --git a/api/v1beta1/l2advertisement_webhook_test.go b/api/v1beta1/l2advertisement_webhook_test.go
index bdb5f1b4..cfc54e4c 100644
--- a/api/v1beta1/l2advertisement_webhook_test.go
+++ b/api/v1beta1/l2advertisement_webhook_test.go
@@ -11,6 +11,7 @@ import (
 )
 
 func TestValidateL2Advertisement(t *testing.T) {
+	MetalLBNamespace = MetalLBTestNameSpace
 	l2Adv := L2Advertisement{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "test-l2adv",
@@ -117,6 +118,18 @@ func TestValidateL2Advertisement(t *testing.T) {
 			},
 			failValidate: true,
 		},
+		{
+			desc: "Validation must fail if created in different namespace",
+			l2Adv: &L2Advertisement{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-l2adv1",
+					Namespace: "default",
+				},
+			},
+			isNew:        true,
+			expected:     nil,
+			failValidate: true,
+		},
 	}
 	for _, test := range tests {
 		var err error
diff --git a/api/v1beta2/bgppeer_webhook_test.go b/api/v1beta2/bgppeer_webhook_test.go
index 9a6aabc0..fc0e4863 100644
--- a/api/v1beta2/bgppeer_webhook_test.go
+++ b/api/v1beta2/bgppeer_webhook_test.go
@@ -13,6 +13,7 @@ import (
 const testNamespace = "namespace"
 
 func TestValidateBGPPeer(t *testing.T) {
+	MetalLBNamespace = testNamespace
 	bgpPeer := BGPPeer{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "test-peer",
@@ -109,6 +110,18 @@ func TestValidateBGPPeer(t *testing.T) {
 			},
 			failValidate: true,
 		},
+		{
+			desc: "Validation must fail if created in different namespace",
+			bgpPeer: &BGPPeer{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-peer1",
+					Namespace: "default",
+				},
+			},
+			isNew:        true,
+			expected:     nil,
+			failValidate: true,
+		},
 	}
 	for _, test := range tests {
 		var err error
diff --git a/e2etest/e2etest_suite_test.go b/e2etest/e2etest_suite_test.go
index f8bcd1c0..ee1686da 100644
--- a/e2etest/e2etest_suite_test.go
+++ b/e2etest/e2etest_suite_test.go
@@ -18,6 +18,7 @@ limitations under the License.
 package e2e
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"os"
@@ -37,6 +38,9 @@ import (
 	"go.universe.tf/metallb/e2etest/pkg/service"
 	"go.universe.tf/metallb/e2etest/webhookstests"
 	internalconfig "go.universe.tf/metallb/internal/config"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/klog"
 	"k8s.io/kubernetes/test/e2e/framework"
@@ -50,6 +54,7 @@ var (
 	useOperator       bool
 	reportPath        string
 	updater           testsconfig.Updater
+	updaterOtherNS    testsconfig.Updater
 )
 
 // handleFlags sets up all flags and parses the command line.
@@ -134,11 +139,27 @@ var _ = ginkgo.BeforeSuite(func() {
 	updater, err = testsconfig.UpdaterForCRs(clientconfig, metallb.Namespace)
 	framework.ExpectNoError(err)
 
+	// for testing namespace validation, we need an existing namespace that's different from the
+	// metallb installation namespace
+	otherNamespace := fmt.Sprintf("%s-other", metallb.Namespace)
+	err = updater.Client().Create(context.Background(), &v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: otherNamespace,
+		},
+	})
+	// ignore failure if namespace already exists, fail for any other errors
+	if err != nil && !errors.IsAlreadyExists(err) {
+		framework.ExpectNoError(err)
+	}
+	updaterOtherNS, err = testsconfig.UpdaterForCRs(clientconfig, otherNamespace)
+	framework.ExpectNoError(err)
+
 	reporter := k8s.InitReporter(framework.TestContext.KubeConfig, reportPath, metallb.Namespace)
 
 	bgptests.ConfigUpdater = updater
 	l2tests.ConfigUpdater = updater
 	webhookstests.ConfigUpdater = updater
+	webhookstests.ConfigUpdaterOtherNS = updaterOtherNS
 	bgptests.Reporter = reporter
 	bgptests.ReportPath = reportPath
 	l2tests.Reporter = reporter
@@ -153,4 +174,19 @@ var _ = ginkgo.AfterSuite(func() {
 	framework.ExpectNoError(err)
 	err = updater.Clean()
 	framework.ExpectNoError(err)
+
+	// delete the namespace created for testing namespace validation
+	nsSpec := v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: updaterOtherNS.Namespace(),
+		},
+	}
+	err = updaterOtherNS.Client().Delete(context.Background(), &nsSpec)
+	// ignore failure if namespace does not exist, fail for any other errors
+	if err != nil && !errors.IsNotFound(err) {
+		framework.ExpectNoError(err)
+	}
+	err = updaterOtherNS.Clean()
+	framework.ExpectNoError(err)
+
 })
diff --git a/e2etest/pkg/config/update.go b/e2etest/pkg/config/update.go
index 9fde5eca..4f980e5c 100644
--- a/e2etest/pkg/config/update.go
+++ b/e2etest/pkg/config/update.go
@@ -19,6 +19,7 @@ type Updater interface {
 	Update(r config.ClusterResources) error
 	Clean() error
 	Client() client.Client
+	Namespace() string
 }
 
 type beta1Updater struct {
@@ -185,3 +186,7 @@ func (o beta1Updater) Clean() error {
 func (o beta1Updater) Client() client.Client {
 	return o.cli
 }
+
+func (o beta1Updater) Namespace() string {
+	return o.namespace
+}
diff --git a/e2etest/webhookstests/webhooks.go b/e2etest/webhookstests/webhooks.go
index 1767a623..57b47406 100644
--- a/e2etest/webhookstests/webhooks.go
+++ b/e2etest/webhookstests/webhooks.go
@@ -20,12 +20,14 @@ package webhookstests
 
 import (
 	"context"
+	"fmt"
 
 	"go.universe.tf/metallb/e2etest/pkg/config"
 	"go.universe.tf/metallb/e2etest/pkg/k8s"
 	"go.universe.tf/metallb/e2etest/pkg/metallb"
 
 	"github.com/onsi/ginkgo"
+	"github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 	"github.com/openshift-kni/k8sreporter"
 
@@ -39,8 +41,9 @@ import (
 )
 
 var (
-	ConfigUpdater config.Updater
-	Reporter      *k8sreporter.KubernetesReporter
+	ConfigUpdater        config.Updater
+	ConfigUpdaterOtherNS config.Updater
+	Reporter             *k8sreporter.KubernetesReporter
 )
 
 var _ = ginkgo.Describe("Webhooks", func() {
@@ -381,3 +384,101 @@ var _ = ginkgo.Describe("Webhooks", func() {
 		})
 	})
 })
+
+var _ = table.DescribeTable("Webhooks namespace validation",
+	func(resources *metallbconfig.ClusterResources) {
+		err := ConfigUpdaterOtherNS.Update(*resources)
+		Expect(err).To(MatchError(ContainSubstring(fmt.Sprintf("resource must be created in %s namespace", metallb.Namespace))))
+	},
+	table.Entry("Should reject creating BFDProfile in a different namespace", &metallbconfig.ClusterResources{
+		BFDProfiles: []metallbv1beta1.BFDProfile{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "bfdprofile-webhooks-test",
+				},
+			},
+		},
+	}),
+	table.Entry("Should reject creating IPAddressPool in a different namespace", &metallbconfig.ClusterResources{
+		Pools: []metallbv1beta1.IPAddressPool{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "webhooks-test1",
+				},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"1.1.1.1-1.1.1.100",
+					},
+				},
+			},
+		},
+	}),
+	table.Entry("Should reject creating Legacy AddressPool in a different namespace", &metallbconfig.ClusterResources{
+		LegacyAddressPools: []metallbv1beta1.AddressPool{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "webhooks-test1",
+				},
+				Spec: metallbv1beta1.AddressPoolSpec{
+					Addresses: []string{
+						"1.1.1.1-1.1.1.100",
+					},
+					Protocol: string(metallbconfig.Layer2),
+				},
+			},
+		},
+	}),
+	table.Entry("Should reject creating BGPPeer in a different namespace", &metallbconfig.ClusterResources{
+		Peers: []metallbv1beta2.BGPPeer{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "webhooks-test",
+				},
+				Spec: metallbv1beta2.BGPPeerSpec{
+					Address: "1.1.1",
+					ASN:     64500,
+					MyASN:   1000,
+				},
+			},
+		},
+	}),
+	table.Entry("Should reject creating BGPAdvertisement in a different namespace", &metallbconfig.ClusterResources{
+		BGPAdvs: []metallbv1beta1.BGPAdvertisement{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "adv-webhooks-test",
+				},
+				Spec: metallbv1beta1.BGPAdvertisementSpec{
+					AggregationLength: pointer.Int32Ptr(26),
+					IPAddressPools:    []string{"pool-webhooks-test"},
+				},
+			},
+		},
+	}),
+	table.Entry("Should reject creating L2Advertisement in a different namespace", &metallbconfig.ClusterResources{
+		L2Advs: []metallbv1beta1.L2Advertisement{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "l2adv-webhooks-test",
+				},
+			},
+		},
+	}),
+	table.Entry("Should reject creating Community in a different namespace", &metallbconfig.ClusterResources{
+		Communities: []metallbv1beta1.Community{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "community-webhooks-test",
+				},
+				Spec: metallbv1beta1.CommunitySpec{
+					Communities: []metallbv1beta1.CommunityAlias{
+						{
+							Name:  "test-community",
+							Value: "1234:1",
+						},
+					},
+				},
+			},
+		},
+	}),
+)