Repository: kubernetes-sigs/kueue
Pull request #439 (2022-11-24 09:04:41)

Add a new field nodeSelector to resourceFlavor to replace original metadata.labels
This is because some components like helm or argocd will add its own labels to owned objects
for management, in this situation, when we use them to deploy resourceFlavor, unexpected labels
will be added to resourceFlavor which will lead to pods pending for corresponding jobs will be added
these labels to their node selector automatically

Signed-off-by: kerthcet <kerthcet@gmail.com>

<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?
/kind feature

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

#### What this PR does / why we need it:

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes https://github.com/kubernetes-sigs/kueue/issues/425

#### Special notes for your reviewer:



k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kueue/pull/439#" title="Author self-approved">kerthcet</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign denkensk for approval by writing `/assign @denkensk` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkueue).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/kueue/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["denkensk"]} -->

Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..5cbd8d423
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,15 @@
+version: 2
+updates:
+
+  # Maintain dependencies for go
+  - package-ecosystem: "gomod"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    labels:
+      - "ok-to-test"
+    ignore:
+      # Ignore major and minor versions for dependencies updates
+      # Allow patches and security updates.
+      - dependency-name: k8s.io/*
+        update-types: ["version-update:semver-major", "version-update:semver-minor"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
index b3da3aeec..c49afb56e 100644
--- a/Makefile
+++ b/Makefile
@@ -143,10 +143,10 @@ test-integration: manifests generate fmt vet envtest ginkgo ## Run tests.
 	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) --arch=amd64 use $(ENVTEST_K8S_VERSION) -p path)" \
 	$(GINKGO) --junit-report=junit.xml --output-dir=$(ARTIFACTS) -v $(INTEGRATION_TARGET)
 
-USE_EXISTING_CLUSTER ?= false
-.PHONY: test-e2e-kind
-test-e2e-kind: manifests generate fmt vet envtest ginkgo kind-image-build
-	E2E_KIND_VERSION=$(E2E_KIND_VERSION) KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) USE_EXISTING_CLUSTER=$(USE_EXISTING_CLUSTER) ARTIFACTS=$(ARTIFACTS) IMAGE_TAG=$(IMAGE_TAG) ./hack/e2e-test.sh
+CREATE_KIND_CLUSTER ?= true
+.PHONY: test-e2e
+test-e2e: kustomize manifests generate fmt vet envtest ginkgo
+	E2E_KIND_VERSION=$(E2E_KIND_VERSION) KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) CREATE_KIND_CLUSTER=$(CREATE_KIND_CLUSTER) ARTIFACTS=$(ARTIFACTS) IMAGE_TAG=$(IMAGE_TAG) ./hack/e2e-test.sh
 
 .PHONY: ci-lint
 ci-lint: golangci-lint
@@ -270,4 +270,3 @@ KIND = $(shell pwd)/bin/kind
 .PHONY: kind
 kind:
 	@GOBIN=$(PROJECT_DIR)/bin GO111MODULE=on $(GO_CMD) install sigs.k8s.io/kind@v0.16.0
-
diff --git a/apis/kueue/v1alpha2/resourceflavor_types.go b/apis/kueue/v1alpha2/resourceflavor_types.go
index e1319d565..65f2dc4ba 100644
--- a/apis/kueue/v1alpha2/resourceflavor_types.go
+++ b/apis/kueue/v1alpha2/resourceflavor_types.go
@@ -25,14 +25,17 @@ import (
 //+kubebuilder:resource:scope=Cluster,shortName={rf}
 
 // ResourceFlavor is the Schema for the resourceflavors API.
-//
-// .metadata.labels associated with this flavor are matched against or
-// converted to node affinity constraints on the workload’s pods.
-// .metadata.labels can be up to 8 elements.
 type ResourceFlavor struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
+	// nodeSelector associated with this flavor. They are matched against or
+	// converted to node affinity constraints on the workload’s pods.
+	//
+	// nodeSelector can be up to 8 elements.
+	// +optional
+	NodeSelector map[string]string `json:"nodeSelector,omitempty"`
+
 	// taints associated with this flavor that workloads must explicitly
 	// “tolerate” to be able to use this flavor.
 	// For example, cloud.provider.com/preemptible="true":NoSchedule
diff --git a/apis/kueue/v1alpha2/zz_generated.deepcopy.go b/apis/kueue/v1alpha2/zz_generated.deepcopy.go
index 1e9200f14..4c46fef3e 100644
--- a/apis/kueue/v1alpha2/zz_generated.deepcopy.go
+++ b/apis/kueue/v1alpha2/zz_generated.deepcopy.go
@@ -358,6 +358,13 @@ func (in *ResourceFlavor) DeepCopyInto(out *ResourceFlavor) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	if in.NodeSelector != nil {
+		in, out := &in.NodeSelector, &out.NodeSelector
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	if in.Taints != nil {
 		in, out := &in.Taints, &out.Taints
 		*out = make([]corev1.Taint, len(*in))
diff --git a/apis/kueue/webhooks/clusterqueue_webhook.go b/apis/kueue/webhooks/clusterqueue_webhook.go
index dd9cb837c..a5a02849d 100644
--- a/apis/kueue/webhooks/clusterqueue_webhook.go
+++ b/apis/kueue/webhooks/clusterqueue_webhook.go
@@ -35,9 +35,6 @@ import (
 )
 
 var (
-	// log is for logging in this package.
-	clusterQueueLog = ctrl.Log.WithName("clusterqueue-webhook")
-
 	queueingStrategies = sets.NewString(string(kueue.StrictFIFO), string(kueue.BestEffortFIFO))
 )
 
@@ -62,8 +59,8 @@ var _ webhook.CustomDefaulter = &ClusterQueueWebhook{}
 // Default implements webhook.CustomDefaulter so a webhook will be registered for the type
 func (w *ClusterQueueWebhook) Default(ctx context.Context, obj runtime.Object) error {
 	cq := obj.(*kueue.ClusterQueue)
-
-	clusterQueueLog.V(5).Info("Applying defaults", "clusterQueue", klog.KObj(cq))
+	log := ctrl.LoggerFrom(ctx).WithName("clusterqueue-webhook")
+	log.V(5).Info("Applying defaults", "clusterQueue", klog.KObj(cq))
 	if !controllerutil.ContainsFinalizer(cq, kueue.ResourceInUseFinalizerName) {
 		controllerutil.AddFinalizer(cq, kueue.ResourceInUseFinalizerName)
 	}
@@ -77,7 +74,8 @@ var _ webhook.CustomValidator = &ClusterQueueWebhook{}
 // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
 func (w *ClusterQueueWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) error {
 	cq := obj.(*kueue.ClusterQueue)
-	clusterQueueLog.V(5).Info("Validating create", "clusterQueue", klog.KObj(cq))
+	log := ctrl.LoggerFrom(ctx).WithName("clusterqueue-webhook")
+	log.V(5).Info("Validating create", "clusterQueue", klog.KObj(cq))
 	allErrs := ValidateClusterQueue(cq)
 	return allErrs.ToAggregate()
 }
@@ -85,7 +83,8 @@ func (w *ClusterQueueWebhook) ValidateCreate(ctx context.Context, obj runtime.Ob
 // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
 func (w *ClusterQueueWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
 	newCQ := newObj.(*kueue.ClusterQueue)
-	clusterQueueLog.V(5).Info("Validating update", "clusterQueue", klog.KObj(newCQ))
+	log := ctrl.LoggerFrom(ctx).WithName("clusterqueue-webhook")
+	log.V(5).Info("Validating update", "clusterQueue", klog.KObj(newCQ))
 	allErrs := ValidateClusterQueue(newCQ)
 	return allErrs.ToAggregate()
 }
diff --git a/apis/kueue/webhooks/localqueue_webhook.go b/apis/kueue/webhooks/localqueue_webhook.go
index 4ec769e33..165a17daf 100644
--- a/apis/kueue/webhooks/localqueue_webhook.go
+++ b/apis/kueue/webhooks/localqueue_webhook.go
@@ -29,9 +29,6 @@ import (
 	kueue "sigs.k8s.io/kueue/apis/kueue/v1alpha2"
 )
 
-// log is for logging in this package.
-var localQueueLog = ctrl.Log.WithName("localqueue-webhook")
-
 type LocalQueueWebhook struct{}
 
 func setupWebhookForLocalQueue(mgr ctrl.Manager) error {
@@ -48,7 +45,8 @@ var _ webhook.CustomValidator = &LocalQueueWebhook{}
 // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
 func (w *LocalQueueWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) error {
 	q := obj.(*kueue.LocalQueue)
-	localQueueLog.V(5).Info("Validating create", "localQueue", klog.KObj(q))
+	log := ctrl.LoggerFrom(ctx).WithName("localqueue-webhook")
+	log.V(5).Info("Validating create", "localQueue", klog.KObj(q))
 	return ValidateLocalQueue(q).ToAggregate()
 }
 
@@ -56,7 +54,8 @@ func (w *LocalQueueWebhook) ValidateCreate(ctx context.Context, obj runtime.Obje
 func (w *LocalQueueWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
 	newQ := newObj.(*kueue.LocalQueue)
 	oldQ := oldObj.(*kueue.LocalQueue)
-	localQueueLog.V(5).Info("Validating update", "localQueue", klog.KObj(newQ))
+	log := ctrl.LoggerFrom(ctx).WithName("localqueue-webhook")
+	log.V(5).Info("Validating update", "localQueue", klog.KObj(newQ))
 	return ValidateLocalQueueUpdate(newQ, oldQ).ToAggregate()
 }
 
diff --git a/apis/kueue/webhooks/resourceflavor_webhook.go b/apis/kueue/webhooks/resourceflavor_webhook.go
index 23e1f8c7e..0f791efea 100644
--- a/apis/kueue/webhooks/resourceflavor_webhook.go
+++ b/apis/kueue/webhooks/resourceflavor_webhook.go
@@ -34,9 +34,6 @@ import (
 	kueue "sigs.k8s.io/kueue/apis/kueue/v1alpha2"
 )
 
-// log is for logging in this package.
-var resourceFlavorLog = ctrl.Log.WithName("resource-flavor-webhook")
-
 type ResourceFlavorWebhook struct{}
 
 func setupWebhookForResourceFlavor(mgr ctrl.Manager) error {
@@ -54,7 +51,8 @@ var _ webhook.CustomDefaulter = &ResourceFlavorWebhook{}
 // Default implements webhook.CustomDefaulter so a webhook will be registered for the type
 func (w *ResourceFlavorWebhook) Default(ctx context.Context, obj runtime.Object) error {
 	rf := obj.(*kueue.ResourceFlavor)
-	resourceFlavorLog.V(5).Info("Applying defaults", "resourceFlavor", klog.KObj(rf))
+	log := ctrl.LoggerFrom(ctx).WithName("resourceflavor-webhook")
+	log.V(5).Info("Applying defaults", "resourceFlavor", klog.KObj(rf))
 
 	if !controllerutil.ContainsFinalizer(rf, kueue.ResourceInUseFinalizerName) {
 		controllerutil.AddFinalizer(rf, kueue.ResourceInUseFinalizerName)
@@ -69,14 +67,16 @@ var _ webhook.CustomValidator = &ResourceFlavorWebhook{}
 // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
 func (w *ResourceFlavorWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) error {
 	rf := obj.(*kueue.ResourceFlavor)
-	resourceFlavorLog.V(5).Info("Validating create", "resourceFlavor", klog.KObj(rf))
+	log := ctrl.LoggerFrom(ctx).WithName("resourceflavor-webhook")
+	log.V(5).Info("Validating create", "resourceFlavor", klog.KObj(rf))
 	return ValidateResourceFlavor(rf).ToAggregate()
 }
 
 // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
 func (w *ResourceFlavorWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
 	newRF := newObj.(*kueue.ResourceFlavor)
-	resourceFlavorLog.V(5).Info("Validating update", "resourceFlavor", klog.KObj(newRF))
+	log := ctrl.LoggerFrom(ctx).WithName("resourceflavor-webhook")
+	log.V(5).Info("Validating update", "resourceFlavor", klog.KObj(newRF))
 	return ValidateResourceFlavor(newRF).ToAggregate()
 }
 
@@ -88,9 +88,11 @@ func (w *ResourceFlavorWebhook) ValidateDelete(ctx context.Context, obj runtime.
 func ValidateResourceFlavor(rf *kueue.ResourceFlavor) field.ErrorList {
 	var allErrs field.ErrorList
 
-	if len(rf.Labels) > 8 {
-		allErrs = append(allErrs, field.TooMany(field.NewPath("metadata", "labels"), len(rf.Labels), 8))
+	nodeSelectorPath := field.NewPath("nodeSelector")
+	if len(rf.NodeSelector) > 8 {
+		allErrs = append(allErrs, field.TooMany(nodeSelectorPath, len(rf.NodeSelector), 8))
 	}
+	allErrs = append(allErrs, metavalidation.ValidateLabels(rf.NodeSelector, nodeSelectorPath)...)
 
 	taintsPath := field.NewPath("taints")
 	if len(rf.Taints) > 8 {
diff --git a/apis/kueue/webhooks/workload_webhook.go b/apis/kueue/webhooks/workload_webhook.go
index 3b20df506..c74cafd16 100644
--- a/apis/kueue/webhooks/workload_webhook.go
+++ b/apis/kueue/webhooks/workload_webhook.go
@@ -33,9 +33,6 @@ import (
 	kueue "sigs.k8s.io/kueue/apis/kueue/v1alpha2"
 )
 
-// log is for logging in this package.
-var workloadlog = ctrl.Log.WithName("workload-webhook")
-
 type WorkloadWebhook struct{}
 
 func setupWebhookForWorkload(mgr ctrl.Manager) error {
@@ -53,7 +50,8 @@ var _ webhook.CustomDefaulter = &WorkloadWebhook{}
 // Default implements webhook.CustomDefaulter so a webhook will be registered for the type
 func (w *WorkloadWebhook) Default(ctx context.Context, obj runtime.Object) error {
 	wl := obj.(*kueue.Workload)
-	workloadlog.V(5).Info("Applying defaults", "workload", klog.KObj(wl))
+	log := ctrl.LoggerFrom(ctx).WithName("workload-webhook")
+	log.V(5).Info("Applying defaults", "workload", klog.KObj(wl))
 
 	if len(wl.Spec.PodSets) == 1 {
 		podSet := &wl.Spec.PodSets[0]
@@ -92,7 +90,8 @@ var _ webhook.CustomValidator = &WorkloadWebhook{}
 // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
 func (w *WorkloadWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) error {
 	wl := obj.(*kueue.Workload)
-	workloadlog.V(5).Info("Validating create", "workload", klog.KObj(wl))
+	log := ctrl.LoggerFrom(ctx).WithName("workload-webhook")
+	log.V(5).Info("Validating create", "workload", klog.KObj(wl))
 	return ValidateWorkload(wl).ToAggregate()
 }
 
@@ -100,7 +99,8 @@ func (w *WorkloadWebhook) ValidateCreate(ctx context.Context, obj runtime.Object
 func (w *WorkloadWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
 	newWL := newObj.(*kueue.Workload)
 	oldWL := oldObj.(*kueue.Workload)
-	workloadlog.V(5).Info("Validating update", "workload", klog.KObj(newWL))
+	log := ctrl.LoggerFrom(ctx).WithName("workload-webhook")
+	log.V(5).Info("Validating update", "workload", klog.KObj(newWL))
 	return ValidateWorkloadUpdate(newWL, oldWL).ToAggregate()
 }
 
diff --git a/config/components/crd/bases/kueue.x-k8s.io_resourceflavors.yaml b/config/components/crd/bases/kueue.x-k8s.io_resourceflavors.yaml
index 835fa3fd7..1900b19cb 100644
--- a/config/components/crd/bases/kueue.x-k8s.io_resourceflavors.yaml
+++ b/config/components/crd/bases/kueue.x-k8s.io_resourceflavors.yaml
@@ -20,10 +20,7 @@ spec:
   - name: v1alpha2
     schema:
       openAPIV3Schema:
-        description: "ResourceFlavor is the Schema for the resourceflavors API. \n
-          .metadata.labels associated with this flavor are matched against or converted
-          to node affinity constraints on the workload’s pods. .metadata.labels can
-          be up to 8 elements."
+        description: ResourceFlavor is the Schema for the resourceflavors API.
         properties:
           apiVersion:
             description: 'APIVersion defines the versioned schema of this representation
@@ -37,6 +34,13 @@ spec:
             type: string
           metadata:
             type: object
+          nodeSelector:
+            additionalProperties:
+              type: string
+            description: "nodeSelector associated with this flavor. They are matched
+              against or converted to node affinity constraints on the workload’s
+              pods. \n nodeSelector can be up to 8 elements."
+            type: object
           taints:
             description: "taints associated with this flavor that workloads must explicitly
               “tolerate” to be able to use this flavor. For example, cloud.provider.com/preemptible=\"true\":NoSchedule
diff --git a/config/components/webhook/manifests.yaml b/config/components/webhook/manifests.yaml
index c34ab41d7..8a3074cf2 100644
--- a/config/components/webhook/manifests.yaml
+++ b/config/components/webhook/manifests.yaml
@@ -63,6 +63,25 @@ webhooks:
     resources:
     - workloads
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /mutate-batch-v1-job
+  failurePolicy: Fail
+  name: mjob.kb.io
+  rules:
+  - apiGroups:
+    - batch
+    apiVersions:
+    - v1
+    operations:
+    - CREATE
+    resources:
+    - jobs
+  sideEffects: None
 ---
 apiVersion: admissionregistration.k8s.io/v1
 kind: ValidatingWebhookConfiguration
@@ -150,3 +169,22 @@ webhooks:
     resources:
     - workloads
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-batch-v1-job
+  failurePolicy: Fail
+  name: vjob.kb.io
+  rules:
+  - apiGroups:
+    - batch
+    apiVersions:
+    - v1
+    operations:
+    - UPDATE
+    resources:
+    - jobs
+  sideEffects: None
diff --git a/docs/concepts/cluster_queue.md b/docs/concepts/cluster_queue.md
index 04080acf5..b14312b89 100644
--- a/docs/concepts/cluster_queue.md
+++ b/docs/concepts/cluster_queue.md
@@ -163,8 +163,8 @@ apiVersion: kueue.x-k8s.io/v1alpha2
 kind: ResourceFlavor
 metadata:
   name: spot
-  labels:
-    instance-type: spot
+nodeSelector:
+  instance-type: spot
 taints:
 - effect: NoSchedule
   key: spot
@@ -179,7 +179,7 @@ ClusterQueue in the `.spec.resources[*].flavors[*].name` field.
 **Requires Kubernetes 1.23 or newer**
 
 To associate a ResourceFlavor with a subset of nodes of your cluster, you can
-configure the `.metadata.labels` field with matching node labels that uniquely identify
+configure the `.nodeSelector` field with matching node labels that uniquely identify
 the nodes. If you are using [cluster autoscaler](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler)
 (or equivalent controllers), make sure it is configured to add those labels when
 adding new nodes.
diff --git a/docs/tasks/administer_cluster_quotas.md b/docs/tasks/administer_cluster_quotas.md
index c816908db..a97328155 100644
--- a/docs/tasks/administer_cluster_quotas.md
+++ b/docs/tasks/administer_cluster_quotas.md
@@ -143,8 +143,8 @@ apiVersion: kueue.x-k8s.io/v1alpha2
 kind: ResourceFlavor
 metadata:
   name: x86
-  labels:
-    cpu-arch: x86
+nodeSelector:
+  cpu-arch: x86
 ```
 
 ```yaml
@@ -153,8 +153,8 @@ apiVersion: kueue.x-k8s.io/v1alpha2
 kind: ResourceFlavor
 metadata:
   name: arm
-  labels:
-    cpu-arch: arm
+nodeSelector:
+  cpu-arch: arm
 ```
 
 To create the ResourceFlavors, run the following command:
diff --git a/go.mod b/go.mod
index c453d2584..3dddb3a66 100644
--- a/go.mod
+++ b/go.mod
@@ -4,16 +4,16 @@ go 1.18
 
 require (
 	github.com/go-logr/logr v1.2.3
-	github.com/google/go-cmp v0.5.8
-	github.com/onsi/ginkgo/v2 v2.1.4
-	github.com/onsi/gomega v1.20.0
-	github.com/open-policy-agent/cert-controller v0.3.0
+	github.com/google/go-cmp v0.5.9
+	github.com/onsi/ginkgo/v2 v2.5.1
+	github.com/onsi/gomega v1.24.0
+	github.com/open-policy-agent/cert-controller v0.5.0
 	github.com/prometheus/client_golang v1.13.0
-	go.uber.org/zap v1.22.0
-	k8s.io/api v0.24.3
-	k8s.io/apimachinery v0.24.3
-	k8s.io/client-go v0.24.3
-	k8s.io/component-base v0.24.3
+	go.uber.org/zap v1.23.0
+	k8s.io/api v0.24.8
+	k8s.io/apimachinery v0.24.8
+	k8s.io/client-go v0.24.8
+	k8s.io/component-base v0.24.8
 	k8s.io/component-helpers v0.24.3
 	k8s.io/klog/v2 v2.60.1
 	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
@@ -64,11 +64,11 @@ require (
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.7.0 // indirect
 	golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
-	golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
+	golang.org/x/net v0.1.0 // indirect
 	golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
-	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
-	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
-	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/sys v0.2.0 // indirect
+	golang.org/x/term v0.1.0 // indirect
+	golang.org/x/text v0.4.0 // indirect
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/go.sum b/go.sum
index 02491dba8..ad7f839c9 100644
--- a/go.sum
+++ b/go.sum
@@ -257,8 +257,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@@ -400,15 +400,15 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
-github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY=
-github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
+github.com/onsi/ginkgo/v2 v2.5.1 h1:auzK7OI497k6x4OvWq+TKAcpcSAlod0doAH72oIN0Jw=
+github.com/onsi/ginkgo/v2 v2.5.1/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q=
-github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
-github.com/open-policy-agent/cert-controller v0.3.0 h1:9eUgN3yYMZsfyW7qdW8+CX9YZCUb5R5JfRTj0cqaSVg=
-github.com/open-policy-agent/cert-controller v0.3.0/go.mod h1:uOQW+2tMU51vSxy1Yt162oVUTMdqLuotC0aObQxrh6k=
+github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg=
+github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
+github.com/open-policy-agent/cert-controller v0.5.0 h1:j8WiSh+UYv2GdxlcxgfXv+QxZQYwdbXV3KsZ4fZsM5A=
+github.com/open-policy-agent/cert-controller v0.5.0/go.mod h1:uOQW+2tMU51vSxy1Yt162oVUTMdqLuotC0aObQxrh6k=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -484,13 +484,16 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
 github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -537,7 +540,8 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
-go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
+go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
@@ -545,8 +549,8 @@ go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95a
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
-go.uber.org/zap v1.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0=
-go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U=
+go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
+go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -646,8 +650,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -749,11 +753,12 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -762,8 +767,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1026,21 +1032,24 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg=
-k8s.io/api v0.24.3 h1:tt55QEmKd6L2k5DP6G/ZzdMQKvG5ro4H4teClqm0sTY=
 k8s.io/api v0.24.3/go.mod h1:elGR/XSZrS7z7cSZPzVWaycpJuGIw57j9b95/1PdJNI=
+k8s.io/api v0.24.8 h1:5bZ6aotI1J+BG2g0U9nBrPy1dOzBeJ/HFOHi6dX+ud4=
+k8s.io/api v0.24.8/go.mod h1:W2RSRCK+eDrYEH4YeuSKrIY90TYYIcW1ojk8Mo6HVOo=
 k8s.io/apiextensions-apiserver v0.24.2 h1:/4NEQHKlEz1MlaK/wHT5KMKC9UKYz6NZz6JE6ov4G6k=
 k8s.io/apiextensions-apiserver v0.24.2/go.mod h1:e5t2GMFVngUEHUd0wuCJzw8YDwZoqZfJiGOW6mm2hLQ=
 k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM=
-k8s.io/apimachinery v0.24.3 h1:hrFiNSA2cBZqllakVYyH/VyEh4B581bQRmqATJSeQTg=
 k8s.io/apimachinery v0.24.3/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM=
+k8s.io/apimachinery v0.24.8 h1:/xehDgfpC4uN7I1GBVvx+Anwb2Jjem+GyJ9F9lSmkBs=
+k8s.io/apimachinery v0.24.8/go.mod h1:WR5z9Lpw2mOAeDg20iSSrEBRQMY0p2YXVdYpUIgSr4o=
 k8s.io/apiserver v0.24.2/go.mod h1:pSuKzr3zV+L+MWqsEo0kHHYwCo77AT5qXbFXP2jbvFI=
 k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30=
-k8s.io/client-go v0.24.3 h1:Nl1840+6p4JqkFWEW2LnMKU667BUxw03REfLAVhuKQY=
 k8s.io/client-go v0.24.3/go.mod h1:AAovolf5Z9bY1wIg2FZ8LPQlEdKHjLI7ZD4rw920BJw=
+k8s.io/client-go v0.24.8 h1:drY++vKWtstFnG5Z1/4KkViBWRFaaiUugOfM4emNvyE=
+k8s.io/client-go v0.24.8/go.mod h1:YrQVH7HVsT8VIsFgzLUrXHVq9RRt13hA9RoDQ8It76g=
 k8s.io/code-generator v0.24.2/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w=
 k8s.io/component-base v0.24.2/go.mod h1:ucHwW76dajvQ9B7+zecZAP3BVqvrHoOxm8olHEg0nmM=
-k8s.io/component-base v0.24.3 h1:u99WjuHYCRJjS1xeLOx72DdRaghuDnuMgueiGMFy1ec=
-k8s.io/component-base v0.24.3/go.mod h1:bqom2IWN9Lj+vwAkPNOv2TflsP1PeVDIwIN0lRthxYY=
+k8s.io/component-base v0.24.8 h1:3qjRp7V+lj8b3HmZP6kF21EaKU0femkj78Gvw7cPU5M=
+k8s.io/component-base v0.24.8/go.mod h1:jYVLUn7NDJs0a8CtuPSudgLEuW/PMrjeCALZZCS7K2E=
 k8s.io/component-helpers v0.24.3 h1:HKZTNf77K96StY2+FAgKvsXvHwoLvexeGDJatyuWlyI=
 k8s.io/component-helpers v0.24.3/go.mod h1:/1WNW8TfBOijQ1ED2uCHb4wtXYWDVNMqUll8h36iNVo=
 k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
diff --git a/main.go b/main.go
index fdd97fadc..fe8896f6c 100644
--- a/main.go
+++ b/main.go
@@ -171,6 +171,10 @@ func setupControllers(mgr ctrl.Manager, cCache *cache.Cache, queues *queue.Manag
 		setupLog.Error(err, "Unable to create webhook", "webhook", failedWebhook)
 		os.Exit(1)
 	}
+	if err := job.SetupWebhook(mgr, job.WithManageJobsWithoutQueueName(manageJobsWithoutQueueName)); err != nil {
+		setupLog.Error(err, "Unable to create webhook", "webhook", "Job")
+		os.Exit(1)
+	}
 	// +kubebuilder:scaffold:builder
 }
 
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go
index 3fb19cb70..8772ce985 100644
--- a/pkg/cache/cache.go
+++ b/pkg/cache/cache.go
@@ -234,7 +234,7 @@ func (c *ClusterQueue) updateLabelKeys(flavors map[string]*kueue.ResourceFlavor)
 		resKeys := sets.NewString()
 		for _, rf := range res.Flavors {
 			if flv, exist := flavors[rf.Name]; exist {
-				for k := range flv.Labels {
+				for k := range flv.NodeSelector {
 					resKeys.Insert(k)
 				}
 			} else {
diff --git a/pkg/controller/workload/job/job_controller.go b/pkg/controller/workload/job/job_controller.go
index bb7e29432..f23b6d8a0 100644
--- a/pkg/controller/workload/job/job_controller.go
+++ b/pkg/controller/workload/job/job_controller.go
@@ -304,7 +304,7 @@ func (r *JobReconciler) getNodeSelectors(ctx context.Context, w *kueue.Workload)
 		if err := r.client.Get(ctx, types.NamespacedName{Name: flvName}, &flv); err != nil {
 			return nil, err
 		}
-		for k, v := range flv.Labels {
+		for k, v := range flv.NodeSelector {
 			nodeSelector[k] = v
 		}
 		processedFlvs.Insert(flvName)
diff --git a/pkg/controller/workload/job/job_webhook.go b/pkg/controller/workload/job/job_webhook.go
new file mode 100644
index 000000000..c3d60a82f
--- /dev/null
+++ b/pkg/controller/workload/job/job_webhook.go
@@ -0,0 +1,109 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package job
+
+import (
+	"context"
+
+	batchv1 "k8s.io/api/batch/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	"k8s.io/klog/v2"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	"sigs.k8s.io/kueue/pkg/util/pointer"
+)
+
+type JobWebhook struct {
+	manageJobsWithoutQueueName bool
+}
+
+// SetupWebhook configures the webhook for batchJob.
+func SetupWebhook(mgr ctrl.Manager, opts ...Option) error {
+	options := defaultOptions
+	for _, opt := range opts {
+		opt(&options)
+	}
+	wh := &JobWebhook{
+		manageJobsWithoutQueueName: options.manageJobsWithoutQueueName,
+	}
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&batchv1.Job{}).
+		WithDefaulter(wh).
+		WithValidator(wh).
+		Complete()
+}
+
+// +kubebuilder:webhook:path=/mutate-batch-v1-job,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch,resources=jobs,verbs=create,versions=v1,name=mjob.kb.io,admissionReviewVersions=v1
+
+var _ webhook.CustomDefaulter = &JobWebhook{}
+
+// Default implements webhook.CustomDefaulter so a webhook will be registered for the type
+func (w *JobWebhook) Default(ctx context.Context, obj runtime.Object) error {
+	job := obj.(*batchv1.Job)
+	log := ctrl.LoggerFrom(ctx).WithName("job-webhook")
+	log.V(5).Info("Applying defaults", "job", klog.KObj(job))
+
+	if queueName(job) == "" && !w.manageJobsWithoutQueueName {
+		return nil
+	}
+
+	if !(*job.Spec.Suspend) {
+		job.Spec.Suspend = pointer.Bool(true)
+	}
+
+	return nil
+}
+
+// +kubebuilder:webhook:path=/validate-batch-v1-job,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch,resources=jobs,verbs=update,versions=v1,name=vjob.kb.io,admissionReviewVersions=v1
+
+var _ webhook.CustomValidator = &JobWebhook{}
+
+// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
+func (w *JobWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
+
+// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
+func (w *JobWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
+	oldJob := oldObj.(*batchv1.Job)
+	newJob := newObj.(*batchv1.Job)
+	log := ctrl.LoggerFrom(ctx).WithName("job-webhook")
+	log.V(5).Info("Validating update", "job", klog.KObj(newJob))
+
+	return validateUpdate(oldJob, newJob)
+}
+
+func validateUpdate(oldJob, newJob *batchv1.Job) error {
+	suspendPath := field.NewPath("job", "spec", "suspend")
+
+	if queueName(oldJob) == "" && queueName(newJob) != "" && !*newJob.Spec.Suspend {
+		return field.Forbidden(suspendPath, "suspend should be true when adding the queue name")
+	}
+
+	if !*newJob.Spec.Suspend && (queueName(oldJob) != queueName(newJob)) {
+		return field.Forbidden(suspendPath, "should not update queue name when job is unsuspend")
+	}
+
+	return nil
+}
+
+// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
+func (w *JobWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go
index 6b4553a76..1533382cb 100644
--- a/pkg/scheduler/scheduler.go
+++ b/pkg/scheduler/scheduler.go
@@ -419,7 +419,7 @@ func findFlavorForCodepResources(
 			status.AppendReason(fmt.Sprintf("untolerated taint %s in flavor %s", taint, flvLimit.Name))
 			continue
 		}
-		if match, err := selector.Match(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Labels: flavor.Labels}}); !match || err != nil {
+		if match, err := selector.Match(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Labels: flavor.NodeSelector}}); !match || err != nil {
 			if err != nil {
 				return "", nil, asStatus(fmt.Errorf("matching affinity flavor %s: %w", flvLimit.Name, err))
 			}


Test patch:
diff --git a/apis/kueue/webhooks/resourceflavor_webhook_test.go b/apis/kueue/webhooks/resourceflavor_webhook_test.go
index a7767b129..e6a270552 100644
--- a/apis/kueue/webhooks/resourceflavor_webhook_test.go
+++ b/apis/kueue/webhooks/resourceflavor_webhook_test.go
@@ -70,7 +70,7 @@ func TestValidateResourceFlavor(t *testing.T) {
 				return m
 			}()).Obj(),
 			wantErr: field.ErrorList{
-				field.TooMany(field.NewPath("metadata", "labels"), 9, 8),
+				field.TooMany(field.NewPath("nodeSelector"), 9, 8),
 			},
 		},
 		{
@@ -89,12 +89,26 @@ func TestValidateResourceFlavor(t *testing.T) {
 				field.TooMany(field.NewPath("taints"), 9, 8),
 			},
 		},
+		{
+			name: "invalid label name",
+			rf:   utiltesting.MakeResourceFlavor("resource-flavor").MultiLabels(map[string]string{"@abc": "foo"}).Obj(),
+			wantErr: field.ErrorList{
+				field.Invalid(field.NewPath("nodeSelector"), "@abc", ""),
+			},
+		},
+		{
+			name: "invalid label value",
+			rf:   utiltesting.MakeResourceFlavor("resource-flavor").MultiLabels(map[string]string{"foo": "@abc"}).Obj(),
+			wantErr: field.ErrorList{
+				field.Invalid(field.NewPath("nodeSelector"), "@abc", ""),
+			},
+		},
 	}
 
 	for _, tc := range testcases {
 		t.Run(tc.name, func(t *testing.T) {
 			gotErr := ValidateResourceFlavor(tc.rf)
-			if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" {
+			if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.IgnoreFields(field.Error{}, "Detail")); diff != "" {
 				t.Errorf("validateResourceFlavorLabels() mismatch (-want +got):\n%s", diff)
 			}
 		})
diff --git a/hack/e2e-test.sh b/hack/e2e-test.sh
index cd2bc2e99..f648654a5 100755
--- a/hack/e2e-test.sh
+++ b/hack/e2e-test.sh
@@ -1,29 +1,56 @@
+#!/usr/bin/env bash
+
+# Copyright 2022 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
 export KUSTOMIZE=$PWD/bin/kustomize
 export GINKGO=$PWD/bin/ginkgo
 export KIND=$PWD/bin/kind
+
 function cleanup {
-    if [ $USE_EXISTING_CLUSTER == 'false' ] 
-    then 
-        $KIND delete cluster --name $KIND_CLUSTER_NAME
+    if [ $CREATE_KIND_CLUSTER == 'true' ]
+    then
+        $KIND delete cluster --name $KIND_CLUSTER_NAME || { echo "You need to run make kind-image-build before this script"; exit -1; }
     fi
     (cd config/components/manager && $KUSTOMIZE edit set image controller=gcr.io/k8s-staging-kueue/kueue:main)
 }
+
 function startup {
-    if [ $USE_EXISTING_CLUSTER == 'false' ] 
-    then 
+    if [ $CREATE_KIND_CLUSTER == 'true' ]
+    then
         $KIND create cluster --name $KIND_CLUSTER_NAME --image $E2E_KIND_VERSION
     fi
 }
+
 function kind_load {
-    $KIND load docker-image $IMAGE_TAG --name $KIND_CLUSTER_NAME
+    if [ $CREATE_KIND_CLUSTER == 'true' ]
+    then
+        $KIND load docker-image $IMAGE_TAG --name $KIND_CLUSTER_NAME
+    fi
 }
+
 function kueue_deploy {
     (cd config/components/manager && $KUSTOMIZE edit set image controller=$IMAGE_TAG)
     kubectl apply -k test/e2e/config
 }
+
 trap cleanup EXIT
 startup
 kind_load
 kueue_deploy
 $GINKGO --junit-report=junit.xml --output-dir=$ARTIFACTS -v ./test/e2e/...
-
diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go
index c017b9eec..a71561f52 100644
--- a/pkg/cache/cache_test.go
+++ b/pkg/cache/cache_test.go
@@ -104,9 +104,9 @@ func TestCacheClusterQueueOperations(t *testing.T) {
 	setup := func(cache *Cache) {
 		cache.AddOrUpdateResourceFlavor(&kueue.ResourceFlavor{
 			ObjectMeta: metav1.ObjectMeta{
-				Name:   "default",
-				Labels: map[string]string{"cpuType": "default"},
+				Name: "default",
 			},
+			NodeSelector: map[string]string{"cpuType": "default"},
 		})
 		for _, c := range initialClusterQueues {
 			if err := cache.AddClusterQueue(context.Background(), &c); err != nil {
@@ -192,9 +192,9 @@ func TestCacheClusterQueueOperations(t *testing.T) {
 				}
 				cache.AddOrUpdateResourceFlavor(&kueue.ResourceFlavor{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:   "default",
-						Labels: map[string]string{"cpuType": "default"},
+						Name: "default",
 					},
+					NodeSelector: map[string]string{"cpuType": "default"},
 				})
 			},
 			wantClusterQueues: map[string]*ClusterQueue{
@@ -307,9 +307,9 @@ func TestCacheClusterQueueOperations(t *testing.T) {
 				}
 				cache.AddOrUpdateResourceFlavor(&kueue.ResourceFlavor{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:   "default",
-						Labels: map[string]string{"cpuType": "default", "region": "central"},
+						Name: "default",
 					},
+					NodeSelector: map[string]string{"cpuType": "default", "region": "central"},
 				})
 			},
 			wantClusterQueues: map[string]*ClusterQueue{
diff --git a/pkg/cache/snapshot_test.go b/pkg/cache/snapshot_test.go
index f140bcf66..a928b6554 100644
--- a/pkg/cache/snapshot_test.go
+++ b/pkg/cache/snapshot_test.go
@@ -153,15 +153,15 @@ func TestSnapshot(t *testing.T) {
 	flavors := []kueue.ResourceFlavor{
 		{
 			ObjectMeta: metav1.ObjectMeta{
-				Name:   "demand",
-				Labels: map[string]string{"foo": "bar", "instance": "demand"},
+				Name: "demand",
 			},
+			NodeSelector: map[string]string{"foo": "bar", "instance": "demand"},
 		},
 		{
 			ObjectMeta: metav1.ObjectMeta{
-				Name:   "spot",
-				Labels: map[string]string{"baz": "bar", "instance": "spot"},
+				Name: "spot",
 			},
+			NodeSelector: map[string]string{"baz": "bar", "instance": "spot"},
 		},
 		{
 			ObjectMeta: metav1.ObjectMeta{Name: "default"},
@@ -387,15 +387,15 @@ func TestSnapshot(t *testing.T) {
 			},
 			"demand": {
 				ObjectMeta: metav1.ObjectMeta{
-					Name:   "demand",
-					Labels: map[string]string{"foo": "bar", "instance": "demand"},
+					Name: "demand",
 				},
+				NodeSelector: map[string]string{"foo": "bar", "instance": "demand"},
 			},
 			"spot": {
 				ObjectMeta: metav1.ObjectMeta{
-					Name:   "spot",
-					Labels: map[string]string{"baz": "bar", "instance": "spot"},
+					Name: "spot",
 				},
+				NodeSelector: map[string]string{"baz": "bar", "instance": "spot"},
 			},
 		},
 		InactiveClusterQueueSets: sets.String{"flavor-nonexistent-cq": {}},
diff --git a/pkg/controller/workload/job/job_webhook_test.go b/pkg/controller/workload/job/job_webhook_test.go
new file mode 100644
index 000000000..da048ef5b
--- /dev/null
+++ b/pkg/controller/workload/job/job_webhook_test.go
@@ -0,0 +1,80 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package job
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	batchv1 "k8s.io/api/batch/v1"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+
+	testingutil "sigs.k8s.io/kueue/pkg/util/testing"
+)
+
+func TestValidateUpdate(t *testing.T) {
+	suspendPath := field.NewPath("job", "spec", "suspend")
+
+	testcases := []struct {
+		name    string
+		oldJob  *batchv1.Job
+		newJob  *batchv1.Job
+		wantErr error
+	}{
+		{
+			name:    "normal update",
+			oldJob:  testingutil.MakeJob("job", "default").Queue("queue").Obj(),
+			newJob:  testingutil.MakeJob("job", "default").Queue("queue").Suspend(false).Obj(),
+			wantErr: nil,
+		},
+		{
+			name:    "add queue name with suspend is false",
+			oldJob:  testingutil.MakeJob("job", "default").Obj(),
+			newJob:  testingutil.MakeJob("job", "default").Queue("queue").Suspend(false).Obj(),
+			wantErr: field.Forbidden(suspendPath, "suspend should be true when adding the queue name"),
+		},
+		{
+			name:    "add queue name with suspend is true",
+			oldJob:  testingutil.MakeJob("job", "default").Obj(),
+			newJob:  testingutil.MakeJob("job", "default").Queue("queue").Suspend(true).Obj(),
+			wantErr: nil,
+		},
+		{
+			name:    "change queue name with suspend is false",
+			oldJob:  testingutil.MakeJob("job", "default").Queue("queue").Obj(),
+			newJob:  testingutil.MakeJob("job", "default").Queue("queue2").Suspend(false).Obj(),
+			wantErr: field.Forbidden(suspendPath, "should not update queue name when job is unsuspend"),
+		},
+		{
+			name:    "change queue name with suspend is true",
+			oldJob:  testingutil.MakeJob("job", "default").Obj(),
+			newJob:  testingutil.MakeJob("job", "default").Queue("queue").Suspend(true).Obj(),
+			wantErr: nil,
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			gotErr := validateUpdate(tc.oldJob, tc.newJob)
+
+			if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" {
+				t.Errorf("validateUpdate() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go
index f87f62a24..f77b17298 100644
--- a/pkg/scheduler/scheduler_test.go
+++ b/pkg/scheduler/scheduler_test.go
@@ -886,27 +886,27 @@ func TestEntryAssignFlavors(t *testing.T) {
 		},
 		"one": {
 			ObjectMeta: metav1.ObjectMeta{
-				Name:   "one",
-				Labels: map[string]string{"type": "one"},
+				Name: "one",
 			},
+			NodeSelector: map[string]string{"type": "one"},
 		},
 		"two": {
 			ObjectMeta: metav1.ObjectMeta{
-				Name:   "two",
-				Labels: map[string]string{"type": "two"},
+				Name: "two",
 			},
+			NodeSelector: map[string]string{"type": "two"},
 		},
 		"b_one": {
 			ObjectMeta: metav1.ObjectMeta{
-				Name:   "b_one",
-				Labels: map[string]string{"b_type": "one"},
+				Name: "b_one",
 			},
+			NodeSelector: map[string]string{"b_type": "one"},
 		},
 		"b_two": {
 			ObjectMeta: metav1.ObjectMeta{
-				Name:   "b_two",
-				Labels: map[string]string{"b_type": "two"},
+				Name: "b_two",
 			},
+			NodeSelector: map[string]string{"b_type": "two"},
 		},
 		"tainted": {
 			ObjectMeta: metav1.ObjectMeta{Name: "tainted"},
diff --git a/pkg/util/testing/wrappers.go b/pkg/util/testing/wrappers.go
index 23e16fcde..8af890f49 100644
--- a/pkg/util/testing/wrappers.go
+++ b/pkg/util/testing/wrappers.go
@@ -395,9 +395,9 @@ type ResourceFlavorWrapper struct{ kueue.ResourceFlavor }
 func MakeResourceFlavor(name string) *ResourceFlavorWrapper {
 	return &ResourceFlavorWrapper{kueue.ResourceFlavor{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:   name,
-			Labels: map[string]string{},
+			Name: name,
 		},
+		NodeSelector: map[string]string{},
 	}}
 }
 
@@ -409,14 +409,14 @@ func (rf *ResourceFlavorWrapper) Obj() *kueue.ResourceFlavor {
 // MultiLabels adds multi labels to the ResourceFlavor.
 func (rf *ResourceFlavorWrapper) MultiLabels(kv map[string]string) *ResourceFlavorWrapper {
 	for k, v := range kv {
-		rf.Labels[k] = v
+		rf.NodeSelector[k] = v
 	}
 	return rf
 }
 
 // Label adds a label to the ResourceFlavor.
 func (rf *ResourceFlavorWrapper) Label(k, v string) *ResourceFlavorWrapper {
-	rf.Labels[k] = v
+	rf.NodeSelector[k] = v
 	return rf
 }
 
diff --git a/test/integration/controller/job/job_controller_test.go b/test/integration/controller/job/job_controller_test.go
index 6e7082e52..6fab50bee 100644
--- a/test/integration/controller/job/job_controller_test.go
+++ b/test/integration/controller/job/job_controller_test.go
@@ -17,9 +17,7 @@ limitations under the License.
 package job
 
 import (
-	"context"
 	"fmt"
-	"path/filepath"
 
 	"github.com/onsi/ginkgo/v2"
 	"github.com/onsi/gomega"
@@ -29,12 +27,9 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/kubernetes/scheme"
-	"k8s.io/client-go/rest"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kueue "sigs.k8s.io/kueue/apis/kueue/v1alpha2"
 	"sigs.k8s.io/kueue/pkg/constants"
-	"sigs.k8s.io/kueue/pkg/controller/workload/job"
 	workloadjob "sigs.k8s.io/kueue/pkg/controller/workload/job"
 	"sigs.k8s.io/kueue/pkg/util/pointer"
 	"sigs.k8s.io/kueue/pkg/util/testing"
@@ -52,20 +47,13 @@ const (
 	priorityValue     = 10
 )
 
-var (
-	cfg       *rest.Config
-	k8sClient client.Client
-	ctx       context.Context
-	fwk       *framework.Framework
-	crdPath   = filepath.Join("..", "..", "..", "..", "config", "components", "crd", "bases")
-)
-
 // +kubebuilder:docs-gen:collapse=Imports
 
 var _ = ginkgo.Describe("Job controller", func() {
+
 	ginkgo.BeforeEach(func() {
 		fwk = &framework.Framework{
-			ManagerSetup: managerSetup(job.WithManageJobsWithoutQueueName(true)),
+			ManagerSetup: managerSetup(workloadjob.WithManageJobsWithoutQueueName(true)),
 			CRDPath:      crdPath,
 		}
 		ctx, cfg, k8sClient = fwk.Setup()
diff --git a/test/integration/controller/job/job_webhook_test.go b/test/integration/controller/job/job_webhook_test.go
new file mode 100644
index 000000000..7633ded7d
--- /dev/null
+++ b/test/integration/controller/job/job_webhook_test.go
@@ -0,0 +1,150 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package job
+
+import (
+	"github.com/onsi/ginkgo/v2"
+	"github.com/onsi/gomega"
+	batchv1 "k8s.io/api/batch/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	"sigs.k8s.io/kueue/pkg/constants"
+	"sigs.k8s.io/kueue/pkg/controller/workload/job"
+	"sigs.k8s.io/kueue/pkg/util/pointer"
+	"sigs.k8s.io/kueue/pkg/util/testing"
+	"sigs.k8s.io/kueue/test/integration/framework"
+)
+
+var _ = ginkgo.Describe("Job Webhook", func() {
+	var ns *corev1.Namespace
+
+	ginkgo.When("With manageJobsWithoutQueueName enabled", func() {
+
+		ginkgo.BeforeEach(func() {
+			fwk = &framework.Framework{
+				ManagerSetup: managerSetup(job.WithManageJobsWithoutQueueName(true)),
+				CRDPath:      crdPath,
+				WebhookPath:  webhookPath,
+			}
+			ctx, cfg, k8sClient = fwk.Setup()
+
+			ns = &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "job-",
+				},
+			}
+			gomega.Expect(k8sClient.Create(ctx, ns)).To(gomega.Succeed())
+		})
+		ginkgo.AfterEach(func() {
+			gomega.Expect(framework.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed())
+			fwk.Teardown()
+		})
+
+		ginkgo.It("Should suspend a Job even no queue name specified", func() {
+			job := testing.MakeJob("job-without-queue-name", ns.Name).Suspend(false).Obj()
+			gomega.Expect(k8sClient.Create(ctx, job)).Should(gomega.Succeed())
+
+			lookupKey := types.NamespacedName{Name: job.Name, Namespace: job.Namespace}
+			createdJob := &batchv1.Job{}
+			gomega.Eventually(func() bool {
+				if err := k8sClient.Get(ctx, lookupKey, createdJob); err != nil {
+					return false
+				}
+				return createdJob.Spec.Suspend != nil && *createdJob.Spec.Suspend
+			}, framework.Timeout, framework.Interval).Should(gomega.BeTrue())
+		})
+
+		ginkgo.It("Should not update unsuspend Job successfully when adding queue name", func() {
+			job := testing.MakeJob("job-without-queue-name", ns.Name).Suspend(false).Obj()
+			gomega.Expect(k8sClient.Create(ctx, job)).Should(gomega.Succeed())
+
+			lookupKey := types.NamespacedName{Name: job.Name, Namespace: job.Namespace}
+			createdJob := &batchv1.Job{}
+			gomega.Expect(k8sClient.Get(ctx, lookupKey, createdJob)).Should(gomega.Succeed())
+
+			createdJob.Annotations = map[string]string{constants.QueueAnnotation: "queue"}
+			createdJob.Spec.Suspend = pointer.Bool(false)
+			gomega.Expect(k8sClient.Update(ctx, createdJob)).ShouldNot(gomega.Succeed())
+		})
+	})
+
+	ginkgo.When("with manageJobsWithoutQueueName disabled", func() {
+
+		ginkgo.BeforeEach(func() {
+			fwk = &framework.Framework{
+				ManagerSetup: managerSetup(job.WithManageJobsWithoutQueueName(false)),
+				CRDPath:      crdPath,
+				WebhookPath:  webhookPath,
+			}
+			ctx, cfg, k8sClient = fwk.Setup()
+
+			ns = &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "job-",
+				},
+			}
+			gomega.Expect(k8sClient.Create(ctx, ns)).To(gomega.Succeed())
+		})
+		ginkgo.AfterEach(func() {
+			gomega.Expect(framework.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed())
+			fwk.Teardown()
+		})
+
+		ginkgo.It("should suspend a Job when created in unsuspend state", func() {
+			job := testing.MakeJob("job-with-queue-name", ns.Name).Suspend(false).Queue("default").Obj()
+			gomega.Expect(k8sClient.Create(ctx, job)).Should(gomega.Succeed())
+
+			lookupKey := types.NamespacedName{Name: job.Name, Namespace: job.Namespace}
+			createdJob := &batchv1.Job{}
+			gomega.Eventually(func() bool {
+				if err := k8sClient.Get(ctx, lookupKey, createdJob); err != nil {
+					return false
+				}
+				return createdJob.Spec.Suspend != nil && *createdJob.Spec.Suspend
+			}, framework.Timeout, framework.Interval).Should(gomega.BeTrue())
+		})
+
+		ginkgo.It("should not suspend a Job when no queue name specified", func() {
+			job := testing.MakeJob("job-without-queue-name", ns.Name).Suspend(false).Obj()
+			gomega.Expect(k8sClient.Create(ctx, job)).Should(gomega.Succeed())
+
+			lookupKey := types.NamespacedName{Name: job.Name, Namespace: job.Namespace}
+			createdJob := &batchv1.Job{}
+			gomega.Eventually(func() bool {
+				if err := k8sClient.Get(ctx, lookupKey, createdJob); err != nil {
+					return false
+				}
+				return createdJob.Spec.Suspend != nil && !(*createdJob.Spec.Suspend)
+			}, framework.Timeout, framework.Interval).Should(gomega.BeTrue())
+		})
+
+		ginkgo.It("should not update unsuspend Job successfully when changing queue name", func() {
+			job := testing.MakeJob("job-with-queue-name", ns.Name).Queue("queue").Obj()
+			gomega.Expect(k8sClient.Create(ctx, job)).Should(gomega.Succeed())
+
+			lookupKey := types.NamespacedName{Name: job.Name, Namespace: job.Namespace}
+			createdJob := &batchv1.Job{}
+			gomega.Expect(k8sClient.Get(ctx, lookupKey, createdJob)).Should(gomega.Succeed())
+
+			createdJob.Annotations[constants.QueueAnnotation] = "queue2"
+			createdJob.Spec.Suspend = pointer.Bool(false)
+			gomega.Expect(k8sClient.Update(ctx, createdJob)).ShouldNot(gomega.Succeed())
+		})
+	})
+})
diff --git a/test/integration/controller/job/suite_test.go b/test/integration/controller/job/suite_test.go
index 50255e529..d537bc0de 100644
--- a/test/integration/controller/job/suite_test.go
+++ b/test/integration/controller/job/suite_test.go
@@ -18,10 +18,13 @@ package job
 
 import (
 	"context"
+	"path/filepath"
 	"testing"
 
 	"github.com/onsi/ginkgo/v2"
 	"github.com/onsi/gomega"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 
 	"sigs.k8s.io/kueue/pkg/cache"
@@ -34,6 +37,15 @@ import (
 	//+kubebuilder:scaffold:imports
 )
 
+var (
+	cfg         *rest.Config
+	k8sClient   client.Client
+	ctx         context.Context
+	fwk         *framework.Framework
+	crdPath     = filepath.Join("..", "..", "..", "..", "config", "components", "crd", "bases")
+	webhookPath = filepath.Join("..", "..", "..", "..", "config", "components", "webhook")
+)
+
 func TestAPIs(t *testing.T) {
 	gomega.RegisterFailHandler(ginkgo.Fail)
 
@@ -53,6 +65,8 @@ func managerSetup(opts ...job.Option) framework.ManagerSetup {
 		gomega.Expect(err).NotTo(gomega.HaveOccurred())
 		err = reconciler.SetupWithManager(mgr)
 		gomega.Expect(err).NotTo(gomega.HaveOccurred())
+		err = job.SetupWebhook(mgr, opts...)
+		gomega.Expect(err).NotTo(gomega.HaveOccurred())
 	}
 }
 
@@ -75,6 +89,8 @@ func managerAndSchedulerSetup(opts ...job.Option) framework.ManagerSetup {
 		err = job.NewReconciler(mgr.GetScheme(), mgr.GetClient(),
 			mgr.GetEventRecorderFor(constants.JobControllerName), opts...).SetupWithManager(mgr)
 		gomega.Expect(err).NotTo(gomega.HaveOccurred())
+		err = job.SetupWebhook(mgr, opts...)
+		gomega.Expect(err).NotTo(gomega.HaveOccurred())
 
 		sched := scheduler.New(queues, cCache, mgr.GetClient(), mgr.GetEventRecorderFor(constants.AdmissionName))
 		go func() {