From 0486e062618f2181857ae7b235dcd4b8be0964e4 Mon Sep 17 00:00:00 2001 From: Cici Huang Date: Tue, 4 Oct 2022 04:46:55 +0000 Subject: [PATCH] Adding new api version of admissionregistration.k8s.io v1alpha1 for CEL in Admission Control --- api/api-rules/violation_exceptions.list | 8 - cmd/kube-apiserver/app/aggregator.go | 1 + hack/.import-aliases | 1 + hack/lib/init.sh | 1 + pkg/api/testing/defaulting_test.go | 256 +-- .../admissionregistration/fuzzer/fuzzer.go | 56 +- .../admissionregistration/install/install.go | 4 +- pkg/apis/admissionregistration/register.go | 4 + pkg/apis/admissionregistration/types.go | 279 ++++ .../admissionregistration/v1/conversion.go | 43 + .../v1alpha1/defaults.go | 56 +- .../v1alpha1/defaults_test.go | 114 +- .../admissionregistration/v1alpha1/doc.go | 3 - .../v1alpha1/register.go | 2 +- .../admissionregistration/v1beta1/defaults.go | 8 - .../validation/validation.go | 243 +++ .../validation/validation_test.go | 1413 +++++++++++++++++ pkg/controlplane/instance.go | 2 + .../default_storage_factory_builder.go | 3 + pkg/printers/internalversion/printers.go | 71 + pkg/printers/internalversion/printers_test.go | 12 + .../rest/storage_apiserver.go | 33 + .../validatingadmissionpolicy/doc.go | 17 + .../storage/storage.go | 65 + .../storage/storage_test.go | 221 +++ .../validatingadmissionpolicy/strategy.go | 96 ++ .../strategy_test.go | 94 ++ .../validatingadmissionpolicybinding/doc.go | 17 + .../storage/storage.go | 65 + .../storage/storage_test.go | 192 +++ .../strategy.go | 96 ++ .../strategy_test.go | 63 + .../api/admissionregistration/v1/types.go | 4 + .../api/admissionregistration/v1alpha1/doc.go | 3 - .../v1alpha1/register.go | 4 + .../admissionregistration/v1alpha1/types.go | 626 +++----- .../admissionregistration/v1beta1/types.go | 74 +- staging/src/k8s.io/api/roundtrip_test.go | 2 + .../pkg/apiserver/schema/cel/compilation.go | 2 + .../pkg/admission/plugin/cel/compiler.go | 220 +++ .../pkg/admission/plugin/cel/compiler_test.go | 125 ++ test/integration/etcd/data.go | 11 + 42 files changed, 3864 insertions(+), 746 deletions(-) create mode 100644 pkg/apis/admissionregistration/v1/conversion.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicy/doc.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicy/strategy.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicybinding/doc.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compiler.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compiler_test.go diff --git a/api/api-rules/violation_exceptions.list b/api/api-rules/violation_exceptions.list index 5dd2f338296..35dd8c2f890 100644 --- a/api/api-rules/violation_exceptions.list +++ b/api/api-rules/violation_exceptions.list @@ -1,10 +1,6 @@ API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,MutatingWebhook,AdmissionReviewVersions API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,MutatingWebhook,Rules API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,MutatingWebhookConfiguration,Webhooks -API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,Rule,APIGroups -API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,Rule,APIVersions -API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,Rule,Resources -API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,RuleWithOperations,Operations API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,ValidatingWebhook,AdmissionReviewVersions API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,ValidatingWebhook,Rules API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,ValidatingWebhookConfiguration,Webhooks @@ -12,10 +8,6 @@ API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1,Webhoo API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,MutatingWebhook,AdmissionReviewVersions API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,MutatingWebhook,Rules API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,MutatingWebhookConfiguration,Webhooks -API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,Rule,APIGroups -API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,Rule,APIVersions -API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,Rule,Resources -API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,RuleWithOperations,Operations API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,ValidatingWebhook,AdmissionReviewVersions API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,ValidatingWebhook,Rules API rule violation: list_type_missing,k8s.io/api/admissionregistration/v1beta1,ValidatingWebhookConfiguration,Webhooks diff --git a/cmd/kube-apiserver/app/aggregator.go b/cmd/kube-apiserver/app/aggregator.go index 4025281e1fc..1789eb35dff 100644 --- a/cmd/kube-apiserver/app/aggregator.go +++ b/cmd/kube-apiserver/app/aggregator.go @@ -275,6 +275,7 @@ var apiVersionPriorities = map[schema.GroupVersion]priority{ {Group: "storage.k8s.io", Version: "v1alpha1"}: {group: 16800, version: 1}, {Group: "apiextensions.k8s.io", Version: "v1"}: {group: 16700, version: 15}, {Group: "admissionregistration.k8s.io", Version: "v1"}: {group: 16700, version: 15}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1"}: {group: 16700, version: 9}, {Group: "scheduling.k8s.io", Version: "v1"}: {group: 16600, version: 15}, {Group: "coordination.k8s.io", Version: "v1"}: {group: 16500, version: 15}, {Group: "node.k8s.io", Version: "v1"}: {group: 16300, version: 15}, diff --git a/hack/.import-aliases b/hack/.import-aliases index 0d722127b68..a28ebd07498 100644 --- a/hack/.import-aliases +++ b/hack/.import-aliases @@ -1,5 +1,6 @@ { "k8s.io/api/admissionregistration/v1": "admissionregistrationv1", + "k8s.io/api/admissionregistration/v1alpha1": "admissionregistrationv1alpha1", "k8s.io/api/admissionregistration/v1beta1": "admissionregistrationv1beta1", "k8s.io/api/admission/v1beta1": "admissionv1beta1", "k8s.io/api/admission/v1": "admissionv1", diff --git a/hack/lib/init.sh b/hack/lib/init.sh index 5b525b8d7e8..51e3d1db195 100755 --- a/hack/lib/init.sh +++ b/hack/lib/init.sh @@ -65,6 +65,7 @@ export KUBE_OUTPUT_HOSTBIN KUBE_AVAILABLE_GROUP_VERSIONS="${KUBE_AVAILABLE_GROUP_VERSIONS:-\ v1 \ admissionregistration.k8s.io/v1 \ +admissionregistration.k8s.io/v1alpha1 \ admissionregistration.k8s.io/v1beta1 \ admission.k8s.io/v1 \ admission.k8s.io/v1beta1 \ diff --git a/pkg/api/testing/defaulting_test.go b/pkg/api/testing/defaulting_test.go index 45385eecc1b..a61d7942839 100644 --- a/pkg/api/testing/defaulting_test.go +++ b/pkg/api/testing/defaulting_test.go @@ -46,132 +46,136 @@ func (o orderedGroupVersionKinds) Less(i, j int) bool { func TestDefaulting(t *testing.T) { // these are the known types with defaulters - you must add to this list if you add a top level defaulter typesWithDefaulting := map[schema.GroupVersionKind]struct{}{ - {Group: "", Version: "v1", Kind: "ConfigMap"}: {}, - {Group: "", Version: "v1", Kind: "ConfigMapList"}: {}, - {Group: "", Version: "v1", Kind: "Endpoints"}: {}, - {Group: "", Version: "v1", Kind: "EndpointsList"}: {}, - {Group: "", Version: "v1", Kind: "EphemeralContainers"}: {}, - {Group: "", Version: "v1", Kind: "Namespace"}: {}, - {Group: "", Version: "v1", Kind: "NamespaceList"}: {}, - {Group: "", Version: "v1", Kind: "Node"}: {}, - {Group: "", Version: "v1", Kind: "NodeList"}: {}, - {Group: "", Version: "v1", Kind: "PersistentVolume"}: {}, - {Group: "", Version: "v1", Kind: "PersistentVolumeList"}: {}, - {Group: "", Version: "v1", Kind: "PersistentVolumeClaim"}: {}, - {Group: "", Version: "v1", Kind: "PersistentVolumeClaimList"}: {}, - {Group: "", Version: "v1", Kind: "Pod"}: {}, - {Group: "", Version: "v1", Kind: "PodList"}: {}, - {Group: "", Version: "v1", Kind: "PodTemplate"}: {}, - {Group: "", Version: "v1", Kind: "PodTemplateList"}: {}, - {Group: "", Version: "v1", Kind: "ReplicationController"}: {}, - {Group: "", Version: "v1", Kind: "ReplicationControllerList"}: {}, - {Group: "", Version: "v1", Kind: "Secret"}: {}, - {Group: "", Version: "v1", Kind: "SecretList"}: {}, - {Group: "", Version: "v1", Kind: "Service"}: {}, - {Group: "", Version: "v1", Kind: "ServiceList"}: {}, - {Group: "apps", Version: "v1beta1", Kind: "StatefulSet"}: {}, - {Group: "apps", Version: "v1beta1", Kind: "StatefulSetList"}: {}, - {Group: "apps", Version: "v1beta2", Kind: "StatefulSet"}: {}, - {Group: "apps", Version: "v1beta2", Kind: "StatefulSetList"}: {}, - {Group: "apps", Version: "v1", Kind: "StatefulSet"}: {}, - {Group: "apps", Version: "v1", Kind: "StatefulSetList"}: {}, - {Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"}: {}, - {Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscalerList"}: {}, - {Group: "autoscaling", Version: "v2", Kind: "HorizontalPodAutoscaler"}: {}, - {Group: "autoscaling", Version: "v2", Kind: "HorizontalPodAutoscalerList"}: {}, - {Group: "autoscaling", Version: "v2beta1", Kind: "HorizontalPodAutoscaler"}: {}, - {Group: "autoscaling", Version: "v2beta1", Kind: "HorizontalPodAutoscalerList"}: {}, - {Group: "autoscaling", Version: "v2beta2", Kind: "HorizontalPodAutoscaler"}: {}, - {Group: "autoscaling", Version: "v2beta2", Kind: "HorizontalPodAutoscalerList"}: {}, - {Group: "batch", Version: "v1", Kind: "CronJob"}: {}, - {Group: "batch", Version: "v1", Kind: "CronJobList"}: {}, - {Group: "batch", Version: "v1", Kind: "Job"}: {}, - {Group: "batch", Version: "v1", Kind: "JobList"}: {}, - {Group: "batch", Version: "v1beta1", Kind: "CronJob"}: {}, - {Group: "batch", Version: "v1beta1", Kind: "CronJobList"}: {}, - {Group: "batch", Version: "v1beta1", Kind: "JobTemplate"}: {}, - {Group: "batch", Version: "v2alpha1", Kind: "CronJob"}: {}, - {Group: "batch", Version: "v2alpha1", Kind: "CronJobList"}: {}, - {Group: "batch", Version: "v2alpha1", Kind: "JobTemplate"}: {}, - {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequest"}: {}, - {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequestList"}: {}, - {Group: "discovery.k8s.io", Version: "v1", Kind: "EndpointSlice"}: {}, - {Group: "discovery.k8s.io", Version: "v1", Kind: "EndpointSliceList"}: {}, - {Group: "discovery.k8s.io", Version: "v1beta1", Kind: "EndpointSlice"}: {}, - {Group: "discovery.k8s.io", Version: "v1beta1", Kind: "EndpointSliceList"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "DaemonSetList"}: {}, - {Group: "apps", Version: "v1beta2", Kind: "DaemonSet"}: {}, - {Group: "apps", Version: "v1beta2", Kind: "DaemonSetList"}: {}, - {Group: "apps", Version: "v1", Kind: "DaemonSet"}: {}, - {Group: "apps", Version: "v1", Kind: "DaemonSetList"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "Deployment"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "DeploymentList"}: {}, - {Group: "apps", Version: "v1beta1", Kind: "Deployment"}: {}, - {Group: "apps", Version: "v1beta1", Kind: "DeploymentList"}: {}, - {Group: "apps", Version: "v1beta2", Kind: "Deployment"}: {}, - {Group: "apps", Version: "v1beta2", Kind: "DeploymentList"}: {}, - {Group: "apps", Version: "v1", Kind: "Deployment"}: {}, - {Group: "apps", Version: "v1", Kind: "DeploymentList"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "Ingress"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "IngressList"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {}, - {Group: "apps", Version: "v1beta2", Kind: "ReplicaSet"}: {}, - {Group: "apps", Version: "v1beta2", Kind: "ReplicaSetList"}: {}, - {Group: "apps", Version: "v1", Kind: "ReplicaSet"}: {}, - {Group: "apps", Version: "v1", Kind: "ReplicaSetList"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSet"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSetList"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicyList"}: {}, - {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {}, - {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBindingList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBindingList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBindingList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBindingList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBindingList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBindingList"}: {}, - {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {}, - {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {}, - {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {}, - {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfigurationList"}: {}, - {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfiguration"}: {}, - {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfigurationList"}: {}, - {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfiguration"}: {}, - {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfigurationList"}: {}, - {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}: {}, - {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicyList"}: {}, - {Group: "networking.k8s.io", Version: "v1beta1", Kind: "Ingress"}: {}, - {Group: "networking.k8s.io", Version: "v1beta1", Kind: "IngressList"}: {}, - {Group: "networking.k8s.io", Version: "v1", Kind: "IngressClass"}: {}, - {Group: "networking.k8s.io", Version: "v1", Kind: "IngressClassList"}: {}, - {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClass"}: {}, - {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClassList"}: {}, - {Group: "storage.k8s.io", Version: "v1beta1", Kind: "CSIDriver"}: {}, - {Group: "storage.k8s.io", Version: "v1beta1", Kind: "CSIDriverList"}: {}, - {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}: {}, - {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClassList"}: {}, - {Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachment"}: {}, - {Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachmentList"}: {}, - {Group: "storage.k8s.io", Version: "v1", Kind: "CSIDriver"}: {}, - {Group: "storage.k8s.io", Version: "v1", Kind: "CSIDriverList"}: {}, - {Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttachment"}: {}, - {Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttachmentList"}: {}, - {Group: "authentication.k8s.io", Version: "v1", Kind: "TokenRequest"}: {}, - {Group: "scheduling.k8s.io", Version: "v1alpha1", Kind: "PriorityClass"}: {}, - {Group: "scheduling.k8s.io", Version: "v1beta1", Kind: "PriorityClass"}: {}, - {Group: "scheduling.k8s.io", Version: "v1", Kind: "PriorityClass"}: {}, - {Group: "scheduling.k8s.io", Version: "v1alpha1", Kind: "PriorityClassList"}: {}, - {Group: "scheduling.k8s.io", Version: "v1beta1", Kind: "PriorityClassList"}: {}, - {Group: "scheduling.k8s.io", Version: "v1", Kind: "PriorityClassList"}: {}, + {Group: "", Version: "v1", Kind: "ConfigMap"}: {}, + {Group: "", Version: "v1", Kind: "ConfigMapList"}: {}, + {Group: "", Version: "v1", Kind: "Endpoints"}: {}, + {Group: "", Version: "v1", Kind: "EndpointsList"}: {}, + {Group: "", Version: "v1", Kind: "EphemeralContainers"}: {}, + {Group: "", Version: "v1", Kind: "Namespace"}: {}, + {Group: "", Version: "v1", Kind: "NamespaceList"}: {}, + {Group: "", Version: "v1", Kind: "Node"}: {}, + {Group: "", Version: "v1", Kind: "NodeList"}: {}, + {Group: "", Version: "v1", Kind: "PersistentVolume"}: {}, + {Group: "", Version: "v1", Kind: "PersistentVolumeList"}: {}, + {Group: "", Version: "v1", Kind: "PersistentVolumeClaim"}: {}, + {Group: "", Version: "v1", Kind: "PersistentVolumeClaimList"}: {}, + {Group: "", Version: "v1", Kind: "Pod"}: {}, + {Group: "", Version: "v1", Kind: "PodList"}: {}, + {Group: "", Version: "v1", Kind: "PodTemplate"}: {}, + {Group: "", Version: "v1", Kind: "PodTemplateList"}: {}, + {Group: "", Version: "v1", Kind: "ReplicationController"}: {}, + {Group: "", Version: "v1", Kind: "ReplicationControllerList"}: {}, + {Group: "", Version: "v1", Kind: "Secret"}: {}, + {Group: "", Version: "v1", Kind: "SecretList"}: {}, + {Group: "", Version: "v1", Kind: "Service"}: {}, + {Group: "", Version: "v1", Kind: "ServiceList"}: {}, + {Group: "apps", Version: "v1beta1", Kind: "StatefulSet"}: {}, + {Group: "apps", Version: "v1beta1", Kind: "StatefulSetList"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "StatefulSet"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "StatefulSetList"}: {}, + {Group: "apps", Version: "v1", Kind: "StatefulSet"}: {}, + {Group: "apps", Version: "v1", Kind: "StatefulSetList"}: {}, + {Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"}: {}, + {Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscalerList"}: {}, + {Group: "autoscaling", Version: "v2", Kind: "HorizontalPodAutoscaler"}: {}, + {Group: "autoscaling", Version: "v2", Kind: "HorizontalPodAutoscalerList"}: {}, + {Group: "autoscaling", Version: "v2beta1", Kind: "HorizontalPodAutoscaler"}: {}, + {Group: "autoscaling", Version: "v2beta1", Kind: "HorizontalPodAutoscalerList"}: {}, + {Group: "autoscaling", Version: "v2beta2", Kind: "HorizontalPodAutoscaler"}: {}, + {Group: "autoscaling", Version: "v2beta2", Kind: "HorizontalPodAutoscalerList"}: {}, + {Group: "batch", Version: "v1", Kind: "CronJob"}: {}, + {Group: "batch", Version: "v1", Kind: "CronJobList"}: {}, + {Group: "batch", Version: "v1", Kind: "Job"}: {}, + {Group: "batch", Version: "v1", Kind: "JobList"}: {}, + {Group: "batch", Version: "v1beta1", Kind: "CronJob"}: {}, + {Group: "batch", Version: "v1beta1", Kind: "CronJobList"}: {}, + {Group: "batch", Version: "v1beta1", Kind: "JobTemplate"}: {}, + {Group: "batch", Version: "v2alpha1", Kind: "CronJob"}: {}, + {Group: "batch", Version: "v2alpha1", Kind: "CronJobList"}: {}, + {Group: "batch", Version: "v2alpha1", Kind: "JobTemplate"}: {}, + {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequest"}: {}, + {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequestList"}: {}, + {Group: "discovery.k8s.io", Version: "v1", Kind: "EndpointSlice"}: {}, + {Group: "discovery.k8s.io", Version: "v1", Kind: "EndpointSliceList"}: {}, + {Group: "discovery.k8s.io", Version: "v1beta1", Kind: "EndpointSlice"}: {}, + {Group: "discovery.k8s.io", Version: "v1beta1", Kind: "EndpointSliceList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "DaemonSetList"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "DaemonSet"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "DaemonSetList"}: {}, + {Group: "apps", Version: "v1", Kind: "DaemonSet"}: {}, + {Group: "apps", Version: "v1", Kind: "DaemonSetList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "Deployment"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "DeploymentList"}: {}, + {Group: "apps", Version: "v1beta1", Kind: "Deployment"}: {}, + {Group: "apps", Version: "v1beta1", Kind: "DeploymentList"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "Deployment"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "DeploymentList"}: {}, + {Group: "apps", Version: "v1", Kind: "Deployment"}: {}, + {Group: "apps", Version: "v1", Kind: "DeploymentList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "Ingress"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "IngressList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "ReplicaSet"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "ReplicaSetList"}: {}, + {Group: "apps", Version: "v1", Kind: "ReplicaSet"}: {}, + {Group: "apps", Version: "v1", Kind: "ReplicaSetList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSet"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSetList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicyList"}: {}, + {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {}, + {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBindingList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBindingList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBindingList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBindingList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBindingList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBindingList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicy"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBinding"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBindingList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfigurationList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfiguration"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfigurationList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfiguration"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfigurationList"}: {}, + {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}: {}, + {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicyList"}: {}, + {Group: "networking.k8s.io", Version: "v1beta1", Kind: "Ingress"}: {}, + {Group: "networking.k8s.io", Version: "v1beta1", Kind: "IngressList"}: {}, + {Group: "networking.k8s.io", Version: "v1", Kind: "IngressClass"}: {}, + {Group: "networking.k8s.io", Version: "v1", Kind: "IngressClassList"}: {}, + {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClass"}: {}, + {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClassList"}: {}, + {Group: "storage.k8s.io", Version: "v1beta1", Kind: "CSIDriver"}: {}, + {Group: "storage.k8s.io", Version: "v1beta1", Kind: "CSIDriverList"}: {}, + {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}: {}, + {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClassList"}: {}, + {Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachment"}: {}, + {Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachmentList"}: {}, + {Group: "storage.k8s.io", Version: "v1", Kind: "CSIDriver"}: {}, + {Group: "storage.k8s.io", Version: "v1", Kind: "CSIDriverList"}: {}, + {Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttachment"}: {}, + {Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttachmentList"}: {}, + {Group: "authentication.k8s.io", Version: "v1", Kind: "TokenRequest"}: {}, + {Group: "scheduling.k8s.io", Version: "v1alpha1", Kind: "PriorityClass"}: {}, + {Group: "scheduling.k8s.io", Version: "v1beta1", Kind: "PriorityClass"}: {}, + {Group: "scheduling.k8s.io", Version: "v1", Kind: "PriorityClass"}: {}, + {Group: "scheduling.k8s.io", Version: "v1alpha1", Kind: "PriorityClassList"}: {}, + {Group: "scheduling.k8s.io", Version: "v1beta1", Kind: "PriorityClassList"}: {}, + {Group: "scheduling.k8s.io", Version: "v1", Kind: "PriorityClassList"}: {}, } f := fuzz.New().NilChance(.5).NumElements(1, 1).RandSource(rand.NewSource(1)) diff --git a/pkg/apis/admissionregistration/fuzzer/fuzzer.go b/pkg/apis/admissionregistration/fuzzer/fuzzer.go index 86db2f44cab..6566bee4784 100644 --- a/pkg/apis/admissionregistration/fuzzer/fuzzer.go +++ b/pkg/apis/admissionregistration/fuzzer/fuzzer.go @@ -35,12 +35,18 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { }, func(obj *admissionregistration.ValidatingWebhook, c fuzz.Continue) { c.FuzzNoCustom(obj) // fuzz self without calling this function again - p := admissionregistration.FailurePolicyType("Fail") - obj.FailurePolicy = &p - m := admissionregistration.MatchPolicyType("Exact") - obj.MatchPolicy = &m - s := admissionregistration.SideEffectClassUnknown - obj.SideEffects = &s + if obj.FailurePolicy == nil { + p := admissionregistration.FailurePolicyType("Fail") + obj.FailurePolicy = &p + } + if obj.MatchPolicy == nil { + m := admissionregistration.MatchPolicyType("Exact") + obj.MatchPolicy = &m + } + if obj.SideEffects == nil { + s := admissionregistration.SideEffectClassUnknown + obj.SideEffects = &s + } if obj.TimeoutSeconds == nil { i := int32(30) obj.TimeoutSeconds = &i @@ -49,19 +55,41 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { }, func(obj *admissionregistration.MutatingWebhook, c fuzz.Continue) { c.FuzzNoCustom(obj) // fuzz self without calling this function again - p := admissionregistration.FailurePolicyType("Fail") - obj.FailurePolicy = &p - m := admissionregistration.MatchPolicyType("Exact") - obj.MatchPolicy = &m - s := admissionregistration.SideEffectClassUnknown - obj.SideEffects = &s - n := admissionregistration.NeverReinvocationPolicy - obj.ReinvocationPolicy = &n + if obj.FailurePolicy == nil { + p := admissionregistration.FailurePolicyType("Fail") + obj.FailurePolicy = &p + } + if obj.MatchPolicy == nil { + m := admissionregistration.MatchPolicyType("Exact") + obj.MatchPolicy = &m + } + if obj.SideEffects == nil { + s := admissionregistration.SideEffectClassUnknown + obj.SideEffects = &s + } + if obj.ReinvocationPolicy == nil { + r := admissionregistration.NeverReinvocationPolicy + obj.ReinvocationPolicy = &r + } if obj.TimeoutSeconds == nil { i := int32(30) obj.TimeoutSeconds = &i } obj.AdmissionReviewVersions = []string{"v1beta1"} }, + func(obj *admissionregistration.ValidatingAdmissionPolicySpec, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + if obj.FailurePolicy == nil { + p := admissionregistration.FailurePolicyType("Fail") + obj.FailurePolicy = &p + } + }, + func(obj *admissionregistration.MatchResources, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + if obj.MatchPolicy == nil { + m := admissionregistration.MatchPolicyType("Exact") + obj.MatchPolicy = &m + } + }, } } diff --git a/pkg/apis/admissionregistration/install/install.go b/pkg/apis/admissionregistration/install/install.go index 67b78812150..46c301db3aa 100644 --- a/pkg/apis/admissionregistration/install/install.go +++ b/pkg/apis/admissionregistration/install/install.go @@ -22,6 +22,7 @@ import ( "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/admissionregistration" v1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1" + "k8s.io/kubernetes/pkg/apis/admissionregistration/v1alpha1" "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1" ) @@ -33,6 +34,7 @@ func init() { func Install(scheme *runtime.Scheme) { utilruntime.Must(admissionregistration.AddToScheme(scheme)) utilruntime.Must(v1beta1.AddToScheme(scheme)) + utilruntime.Must(v1alpha1.AddToScheme(scheme)) utilruntime.Must(v1.AddToScheme(scheme)) - utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion)) + utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion, v1alpha1.SchemeGroupVersion)) } diff --git a/pkg/apis/admissionregistration/register.go b/pkg/apis/admissionregistration/register.go index a1a1c45d270..a69343e20b4 100644 --- a/pkg/apis/admissionregistration/register.go +++ b/pkg/apis/admissionregistration/register.go @@ -51,6 +51,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ValidatingWebhookConfigurationList{}, &MutatingWebhookConfiguration{}, &MutatingWebhookConfigurationList{}, + &ValidatingAdmissionPolicy{}, + &ValidatingAdmissionPolicyList{}, + &ValidatingAdmissionPolicyBinding{}, + &ValidatingAdmissionPolicyBindingList{}, ) return nil } diff --git a/pkg/apis/admissionregistration/types.go b/pkg/apis/admissionregistration/types.go index 46e9626ad5b..58eed901845 100644 --- a/pkg/apis/admissionregistration/types.go +++ b/pkg/apis/admissionregistration/types.go @@ -115,6 +115,285 @@ const ( // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// ValidatingAdmissionPolicy describes the definition of an admission validation policy that accepts or rejects an object without changing it. +type ValidatingAdmissionPolicy struct { + metav1.TypeMeta + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta + // Specification of the desired behavior of the ValidatingAdmissionPolicy. + Spec ValidatingAdmissionPolicySpec +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ValidatingAdmissionPolicyList is a list of ValidatingAdmissionPolicy. +type ValidatingAdmissionPolicyList struct { + metav1.TypeMeta + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta + // List of ValidatingAdmissionPolicy. + Items []ValidatingAdmissionPolicy +} + +// ValidatingAdmissionPolicySpec is the specification of the desired behavior of the AdmissionPolicy. +type ValidatingAdmissionPolicySpec struct { + // ParamKind specifies the kind of resources used to parameterize this policy. + // If absent, there are no parameters for this policy and the param CEL variable will not be provided to validation expressions. + // If ParamKind refers to a non-existent kind, this policy definition is mis-configured and the FailurePolicy is applied. + // If paramKind is specified but paramRef is unset in ValidatingAdmissionPolicyBinding, the params variable will be null. + // +optional + ParamKind *ParamKind + + // MatchConstraints specifies what resources this policy is designed to validate. + // The AdmissionPolicy cares about a request if it matches _all_ Constraint. + // However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API + // ValidatingAdmissionPolicy cannot match ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding. + // Required. + MatchConstraints *MatchResources + + // Validations contain CEL expressions which is used to apply the validation. + // A minimum of one validation is required for a policy definition. + // Required. + Validations []Validation + + // FailurePolicy defines how to handle failures for the admission policy. + // Failures can occur from invalid or mis-configured policy definitions or bindings. + // A policy is invalid if spec.paramKind refers to a non-existent Kind. + // A binding is invalid if spec.paramRef.name refers to a non-existent resource. + // Allowed values are Ignore or Fail. Defaults to Fail. + // +optional + FailurePolicy *FailurePolicyType +} + +// ParamKind is a tuple of Group Kind and Version. +type ParamKind struct { + // APIVersion is the API group version the resources belong to. + // In format of "group/version". + // Required. + APIVersion string + + // Kind is the API kind the resources belong to. + // Required. + Kind string +} + +// Validation specifies the CEL expression which is used to apply the validation. +type Validation struct { + // Expression represents the expression which will be evaluated by CEL. + // ref: https://github.com/google/cel-spec + // CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables: + // + //'object' - The object from the incoming request. The value is null for DELETE requests. + //'oldObject' - The existing object. The value is null for CREATE requests. + //'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + //'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // + // The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the + // object. No other metadata properties are accessible. + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Accessible property names are escaped according to the following rules when accessed in the expression: + // - '__' escapes to '__underscores__' + // - '.' escapes to '__dot__' + // - '-' escapes to '__dash__' + // - '/' escapes to '__slash__' + // - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are: + // "true", "false", "null", "in", "as", "break", "const", "continue", "else", "for", "function", "if", + // "import", "let", "loop", "package", "namespace", "return". + // Examples: + // - Expression accessing a property named "namespace": {"Expression": "object.__namespace__ > 0"} + // - Expression accessing a property named "x-prop": {"Expression": "object.x__dash__prop > 0"} + // - Expression accessing a property named "redact__d": {"Expression": "object.redact__underscores__d > 0"} + // + // Equality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. + // Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type: + // - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and + // non-intersecting elements in `Y` are appended, retaining their partial order. + // - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values + // are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with + // non-intersecting keys are appended, retaining their partial order. + // Required. + Expression string + // Message represents the message displayed when validation fails. The message is required if the Expression contains + // line breaks. The message must not contain line breaks. + // If unset, the message is "failed rule: {Rule}". + // e.g. "must be a URL with the host matching spec.host" + // If ExpressMessage is specified, Message will be ignored + // If the Expression contains line breaks. Eith Message or ExpressMessage is required. + // The message must not contain line breaks. + // If unset, the message is "failed Expression: {Expression}". + // +optional + Message string + // Reason represents a machine-readable description of why this validation failed. + // If this is the first validation in the list to fail, this reason, as well as the + // corresponding HTTP response code, are used in the + // HTTP response to the client. + // The currently supported reasons are: "Unauthorized", "Forbidden", "Invalid", "RequestEntityTooLarge". + // If not set, StatusReasonInvalid is used in the response to the client. + // +optional + Reason *metav1.StatusReason +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ValidatingAdmissionPolicyBinding binds the ValidatingAdmissionPolicy with paramerized resources. +// ValidatingAdmissionPolicyBinding and parameter CRDs together define how cluster administrators configure policies for clusters. +type ValidatingAdmissionPolicyBinding struct { + metav1.TypeMeta + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta + // Specification of the desired behavior of the ValidatingAdmissionPolicyBinding. + Spec ValidatingAdmissionPolicyBindingSpec +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ValidatingAdmissionPolicyBindingList is a list of PolicyBinding. +type ValidatingAdmissionPolicyBindingList struct { + metav1.TypeMeta + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta + // List of PolicyBinding. + Items []ValidatingAdmissionPolicyBinding +} + +// ValidatingAdmissionPolicyBindingSpec is the specification of the ValidatingAdmissionPolicyBinding. +type ValidatingAdmissionPolicyBindingSpec struct { + // PolicyName references a ValidatingAdmissionPolicy name which the ValidatingAdmissionPolicyBinding binds to. + // If the referenced resource does not exist, this binding is considered invalid and will be ignored + // Required. + PolicyName string + + // ParamRef specifies the parameter resource used to configure the admission control policy. + // It should point to a resource of the type specified in ParamKind of the bound ValidatingAdmissionPolicy. + // If the policy specifies a ParamKind and the resource referred to by ParamRef does not exist, this binding is considered mis-configured and the FailurePolicy of the ValidatingAdmissionPolicy applied. + // +optional + ParamRef *ParamRef + + // MatchResources declares what resources match this binding and will be validated by it. + // Note that this is intersected with the policy's matchConstraints, so only requests that are matched by the policy can be selected by this. + // If this is unset, all resources matched by the policy are validated by this binding + // When resourceRules is unset, it does not constrain resource matching. If a resource is matched by the other fields of this object, it will be validated. + // Note that this is differs from ValidatingAdmissionPolicy matchConstraints, where resourceRules are required. + // +optional + MatchResources *MatchResources +} + +// ParamRef references a parameter resource +type ParamRef struct { + // Name of the resource being referenced. + Name string + // Namespace of the referenced resource. + // Should be empty for the cluster-scoped resources + // +optional + Namespace string +} + +// MatchResources decides whether to run the admission control policy on an object based +// on whether it meets the match criteria. +// The exclude rules take precedence over include rules (if a resource matches both, it is excluded) +type MatchResources struct { + // NamespaceSelector decides whether to run the admission control policy on an object based + // on whether the namespace for that object matches the selector. If the + // object itself is a namespace, the matching is performed on + // object.metadata.labels. If the object is another cluster scoped resource, + // it never skips the policy. + // + // For example, to run the webhook on any objects whose namespace is not + // associated with "runlevel" of "0" or "1"; you will set the selector as + // follows: + // "namespaceSelector": { + // "matchExpressions": [ + // { + // "key": "runlevel", + // "operator": "NotIn", + // "values": [ + // "0", + // "1" + // ] + // } + // ] + // } + // + // If instead you want to only run the policy on any objects whose + // namespace is associated with the "environment" of "prod" or "staging"; + // you will set the selector as follows: + // "namespaceSelector": { + // "matchExpressions": [ + // { + // "key": "environment", + // "operator": "In", + // "values": [ + // "prod", + // "staging" + // ] + // } + // ] + // } + // + // See + // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + // for more examples of label selectors. + // + // Default to the empty LabelSelector, which matches everything. + // +optional + NamespaceSelector *metav1.LabelSelector + // ObjectSelector decides whether to run the validation based on if the + // object has matching labels. objectSelector is evaluated against both + // the oldObject and newObject that would be sent to the cel validation, and + // is considered to match if either object matches the selector. A null + // object (oldObject in the case of create, or newObject in the case of + // delete) or an object that cannot have labels (like a + // DeploymentRollback or a PodProxyOptions object) is not considered to + // match. + // Use the object selector only if the webhook is opt-in, because end + // users may skip the admission webhook by setting the labels. + // Default to the empty LabelSelector, which matches everything. + // +optional + ObjectSelector *metav1.LabelSelector + // ResourceRules describes what operations on what resources/subresources the ValidatingAdmissionPolicy matches. + // The policy cares about an operation if it matches _any_ Rule. + // +optional + ResourceRules []NamedRuleWithOperations + // ExcludeResourceRules describes what operations on what resources/subresources the ValidatingAdmissionPolicy should not care about. + // The exclude rules take precedence over include rules (if a resource matches both, it is excluded) + // +optional + ExcludeResourceRules []NamedRuleWithOperations + // matchPolicy defines how the "MatchResources" list is used to match incoming requests. + // Allowed values are "Exact" or "Equivalent". + // + // - Exact: match a request only if it exactly matches a specified rule. + // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, + // but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, + // a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the ValidatingAdmissionPolicy. + // + // - Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. + // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, + // and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, + // a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the ValidatingAdmissionPolicy. + // + // Defaults to "Equivalent" + // +optional + MatchPolicy *MatchPolicyType +} + +// NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames. +type NamedRuleWithOperations struct { + // ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. + // +optional + ResourceNames []string + // RuleWithOperations is a tuple of Operations and Resources. + RuleWithOperations RuleWithOperations +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + // ValidatingWebhookConfiguration describes the configuration of an admission webhook that accepts or rejects and object without changing it. type ValidatingWebhookConfiguration struct { metav1.TypeMeta diff --git a/pkg/apis/admissionregistration/v1/conversion.go b/pkg/apis/admissionregistration/v1/conversion.go new file mode 100644 index 00000000000..ca5f603229a --- /dev/null +++ b/pkg/apis/admissionregistration/v1/conversion.go @@ -0,0 +1,43 @@ +/* +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 v1 + +import ( + v1 "k8s.io/api/admissionregistration/v1" + conversion "k8s.io/apimachinery/pkg/conversion" + admissionregistration "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +// Convert_admissionregistration_Rule_To_v1_Rule is an autogenerated conversion function. +func Convert_admissionregistration_Rule_To_v1_Rule(in *admissionregistration.Rule, out *v1.Rule, s conversion.Scope) error { + return autoConvert_admissionregistration_Rule_To_v1_Rule(in, out, s) +} + +// Convert_v1_Rule_To_admissionregistration_Rule is an autogenerated conversion function. +func Convert_v1_Rule_To_admissionregistration_Rule(in *v1.Rule, out *admissionregistration.Rule, s conversion.Scope) error { + return autoConvert_v1_Rule_To_admissionregistration_Rule(in, out, s) +} + +// Convert_admissionregistration_RuleWithOperations_To_v1_RuleWithOperations is an autogenerated conversion function. +func Convert_admissionregistration_RuleWithOperations_To_v1_RuleWithOperations(in *admissionregistration.RuleWithOperations, out *v1.RuleWithOperations, s conversion.Scope) error { + return autoConvert_admissionregistration_RuleWithOperations_To_v1_RuleWithOperations(in, out, s) +} + +// Convert_v1_RuleWithOperations_To_admissionregistration_RuleWithOperations is an autogenerated conversion function. +func Convert_v1_RuleWithOperations_To_admissionregistration_RuleWithOperations(in *v1.RuleWithOperations, out *admissionregistration.RuleWithOperations, s conversion.Scope) error { + return autoConvert_v1_RuleWithOperations_To_admissionregistration_RuleWithOperations(in, out, s) +} diff --git a/pkg/apis/admissionregistration/v1alpha1/defaults.go b/pkg/apis/admissionregistration/v1alpha1/defaults.go index 45b2885f768..85d12b128f7 100644 --- a/pkg/apis/admissionregistration/v1alpha1/defaults.go +++ b/pkg/apis/admissionregistration/v1alpha1/defaults.go @@ -20,19 +20,22 @@ import ( admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - utilpointer "k8s.io/utils/pointer" ) func addDefaultingFuncs(scheme *runtime.Scheme) error { return RegisterDefaults(scheme) } -// SetDefaults_ValidatingWebhook sets defaults for webhook validating -func SetDefaults_ValidatingWebhook(obj *admissionregistrationv1alpha1.ValidatingWebhook) { +// SetDefaults_ValidatingAdmissionPolicySpec sets defaults for ValidatingAdmissionPolicySpec +func SetDefaults_ValidatingAdmissionPolicySpec(obj *admissionregistrationv1alpha1.ValidatingAdmissionPolicySpec) { if obj.FailurePolicy == nil { policy := admissionregistrationv1alpha1.Fail obj.FailurePolicy = &policy } +} + +// SetDefaults_MatchResources sets defaults for MatchResources +func SetDefaults_MatchResources(obj *admissionregistrationv1alpha1.MatchResources) { if obj.MatchPolicy == nil { policy := admissionregistrationv1alpha1.Equivalent obj.MatchPolicy = &policy @@ -45,51 +48,4 @@ func SetDefaults_ValidatingWebhook(obj *admissionregistrationv1alpha1.Validating selector := metav1.LabelSelector{} obj.ObjectSelector = &selector } - if obj.TimeoutSeconds == nil { - obj.TimeoutSeconds = new(int32) - *obj.TimeoutSeconds = 10 - } -} - -// SetDefaults_MutatingWebhook sets defaults for webhook mutating -func SetDefaults_MutatingWebhook(obj *admissionregistrationv1alpha1.MutatingWebhook) { - if obj.FailurePolicy == nil { - policy := admissionregistrationv1alpha1.Fail - obj.FailurePolicy = &policy - } - if obj.MatchPolicy == nil { - policy := admissionregistrationv1alpha1.Equivalent - obj.MatchPolicy = &policy - } - if obj.NamespaceSelector == nil { - selector := metav1.LabelSelector{} - obj.NamespaceSelector = &selector - } - if obj.ObjectSelector == nil { - selector := metav1.LabelSelector{} - obj.ObjectSelector = &selector - } - if obj.TimeoutSeconds == nil { - obj.TimeoutSeconds = new(int32) - *obj.TimeoutSeconds = 10 - } - if obj.ReinvocationPolicy == nil { - never := admissionregistrationv1alpha1.NeverReinvocationPolicy - obj.ReinvocationPolicy = &never - } -} - -// SetDefaults_Rule sets defaults for webhook rule -func SetDefaults_Rule(obj *admissionregistrationv1alpha1.Rule) { - if obj.Scope == nil { - s := admissionregistrationv1alpha1.AllScopes - obj.Scope = &s - } -} - -// SetDefaults_ServiceReference sets defaults for Webhook's ServiceReference -func SetDefaults_ServiceReference(obj *admissionregistrationv1alpha1.ServiceReference) { - if obj.Port == nil { - obj.Port = utilpointer.Int32Ptr(443) - } } diff --git a/pkg/apis/admissionregistration/v1alpha1/defaults_test.go b/pkg/apis/admissionregistration/v1alpha1/defaults_test.go index 7f132e4c6cf..9315118dd8b 100644 --- a/pkg/apis/admissionregistration/v1alpha1/defaults_test.go +++ b/pkg/apis/admissionregistration/v1alpha1/defaults_test.go @@ -27,14 +27,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api/legacyscheme" _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" - utilpointer "k8s.io/utils/pointer" ) -func TestDefaultAdmissionWebhook(t *testing.T) { +func TestDefaultAdmissionPolicy(t *testing.T) { fail := v1alpha1.Fail equivalent := v1alpha1.Equivalent - never := v1alpha1.NeverReinvocationPolicy - ten := int32(10) allScopes := v1alpha1.AllScopes tests := []struct { @@ -43,80 +40,67 @@ func TestDefaultAdmissionWebhook(t *testing.T) { expected runtime.Object }{ { - name: "ValidatingWebhookConfiguration", - original: &v1alpha1.ValidatingWebhookConfiguration{ - Webhooks: []v1alpha1.ValidatingWebhook{{}}, + name: "ValidatingAdmissionPolicy", + original: &v1alpha1.ValidatingAdmissionPolicy{ + Spec: v1alpha1.ValidatingAdmissionPolicySpec{ + MatchConstraints: &v1alpha1.MatchResources{}, + }, }, - expected: &v1alpha1.ValidatingWebhookConfiguration{ - Webhooks: []v1alpha1.ValidatingWebhook{{ - FailurePolicy: &fail, - MatchPolicy: &equivalent, - TimeoutSeconds: &ten, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - }}, + expected: &v1alpha1.ValidatingAdmissionPolicy{ + Spec: v1alpha1.ValidatingAdmissionPolicySpec{ + MatchConstraints: &v1alpha1.MatchResources{ + MatchPolicy: &equivalent, + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + }, + FailurePolicy: &fail, + }, }, }, { - name: "MutatingWebhookConfiguration", - original: &v1alpha1.MutatingWebhookConfiguration{ - Webhooks: []v1alpha1.MutatingWebhook{{}}, + name: "ValidatingAdmissionPolicyBinding", + original: &v1alpha1.ValidatingAdmissionPolicyBinding{ + Spec: v1alpha1.ValidatingAdmissionPolicyBindingSpec{ + MatchResources: &v1alpha1.MatchResources{}, + }, }, - expected: &v1alpha1.MutatingWebhookConfiguration{ - Webhooks: []v1alpha1.MutatingWebhook{{ - FailurePolicy: &fail, - MatchPolicy: &equivalent, - ReinvocationPolicy: &never, - TimeoutSeconds: &ten, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - }}, + expected: &v1alpha1.ValidatingAdmissionPolicyBinding{ + Spec: v1alpha1.ValidatingAdmissionPolicyBindingSpec{ + MatchResources: &v1alpha1.MatchResources{ + MatchPolicy: &equivalent, + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + }, + }, }, }, { name: "scope=*", - original: &v1alpha1.MutatingWebhookConfiguration{ - Webhooks: []v1alpha1.MutatingWebhook{{ - Rules: []v1alpha1.RuleWithOperations{{}}, - }}, - }, - expected: &v1alpha1.MutatingWebhookConfiguration{ - Webhooks: []v1alpha1.MutatingWebhook{{ - Rules: []v1alpha1.RuleWithOperations{{Rule: v1alpha1.Rule{ - Scope: &allScopes, // defaulted - }}}, - FailurePolicy: &fail, - MatchPolicy: &equivalent, - ReinvocationPolicy: &never, - TimeoutSeconds: &ten, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - }}, - }, - }, - { - name: "port=443", - original: &v1alpha1.MutatingWebhookConfiguration{ - Webhooks: []v1alpha1.MutatingWebhook{{ - ClientConfig: v1alpha1.WebhookClientConfig{ - Service: &v1alpha1.ServiceReference{}, + original: &v1alpha1.ValidatingAdmissionPolicy{ + Spec: v1alpha1.ValidatingAdmissionPolicySpec{ + MatchConstraints: &v1alpha1.MatchResources{ + ResourceRules: []v1alpha1.NamedRuleWithOperations{{}}, }, - }}, + }, }, - expected: &v1alpha1.MutatingWebhookConfiguration{ - Webhooks: []v1alpha1.MutatingWebhook{{ - ClientConfig: v1alpha1.WebhookClientConfig{ - Service: &v1alpha1.ServiceReference{ - Port: utilpointer.Int32Ptr(443), // defaulted + expected: &v1alpha1.ValidatingAdmissionPolicy{ + Spec: v1alpha1.ValidatingAdmissionPolicySpec{ + MatchConstraints: &v1alpha1.MatchResources{ + MatchPolicy: &equivalent, + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + ResourceRules: []v1alpha1.NamedRuleWithOperations{ + { + RuleWithOperations: v1alpha1.RuleWithOperations{ + Rule: v1alpha1.Rule{ + Scope: &allScopes, // defaulted + }, + }, + }, }, }, - FailurePolicy: &fail, - MatchPolicy: &equivalent, - ReinvocationPolicy: &never, - TimeoutSeconds: &ten, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - }}, + FailurePolicy: &fail, + }, }, }, } diff --git a/pkg/apis/admissionregistration/v1alpha1/doc.go b/pkg/apis/admissionregistration/v1alpha1/doc.go index 37e46330c91..2fec4005ee9 100644 --- a/pkg/apis/admissionregistration/v1alpha1/doc.go +++ b/pkg/apis/admissionregistration/v1alpha1/doc.go @@ -21,7 +21,4 @@ limitations under the License. // +groupName=admissionregistration.k8s.io // Package v1alpha1 is the v1alpha1 version of the API. -// AdmissionConfiguration and AdmissionPluginConfiguration are legacy static admission plugin configuration -// ValidatingWebhookConfiguration, and MutatingWebhookConfiguration are for the -// new dynamic admission controller configuration. package v1alpha1 // import "k8s.io/kubernetes/pkg/apis/admissionregistration/v1alpha1" diff --git a/pkg/apis/admissionregistration/v1alpha1/register.go b/pkg/apis/admissionregistration/v1alpha1/register.go index fdbe44af186..c44ff41fe16 100644 --- a/pkg/apis/admissionregistration/v1alpha1/register.go +++ b/pkg/apis/admissionregistration/v1alpha1/register.go @@ -25,7 +25,7 @@ import ( const GroupName = "admissionregistration.k8s.io" // SchemeGroupVersion is group version used to register these objects -var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { diff --git a/pkg/apis/admissionregistration/v1beta1/defaults.go b/pkg/apis/admissionregistration/v1beta1/defaults.go index f0796fc9b73..561a549cdd5 100644 --- a/pkg/apis/admissionregistration/v1beta1/defaults.go +++ b/pkg/apis/admissionregistration/v1beta1/defaults.go @@ -97,14 +97,6 @@ func SetDefaults_MutatingWebhook(obj *admissionregistrationv1beta1.MutatingWebho } } -// SetDefaults_Rule sets defaults for webhook rule -func SetDefaults_Rule(obj *admissionregistrationv1beta1.Rule) { - if obj.Scope == nil { - s := admissionregistrationv1beta1.AllScopes - obj.Scope = &s - } -} - // SetDefaults_ServiceReference sets defaults for Webhook's ServiceReference func SetDefaults_ServiceReference(obj *admissionregistrationv1beta1.ServiceReference) { if obj.Port == nil { diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index 2f8bbc311db..6a07b0b5f48 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -18,13 +18,18 @@ package validation import ( "fmt" + "regexp" "strings" genericvalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/api/validation/path" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/util/sets" utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" + plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/util/webhook" "k8s.io/kubernetes/pkg/apis/admissionregistration" admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1" @@ -390,6 +395,12 @@ var supportedReinvocationPolicies = sets.NewString( string(admissionregistration.IfNeededReinvocationPolicy), ) +var supportedValidationPolicyReason = sets.NewString( + string(metav1.StatusReasonForbidden), + string(metav1.StatusReasonInvalid), + string(metav1.StatusReasonRequestEntityTooLarge), +) + func hasWildcardOperation(operations []admissionregistration.OperationType) bool { for _, o := range operations { if o == admissionregistration.OperationAll { @@ -514,3 +525,235 @@ func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistratio requireUniqueWebhookNames: mutatingHasUniqueWebhookNames(oldC.Webhooks), }) } + +// ValidateValidatingAdmissionPolicy validates a ValidatingAdmissionPolicy before creation. +func ValidateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList { + return validateValidatingAdmissionPolicy(p) +} + +func validateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList { + allErrors := genericvalidation.ValidateObjectMeta(&p.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) + allErrors = append(allErrors, validateValidatingAdmissionPolicySpec(&p.Spec, field.NewPath("spec"))...) + return allErrors +} + +func validateValidatingAdmissionPolicySpec(spec *admissionregistration.ValidatingAdmissionPolicySpec, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + if spec.FailurePolicy == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("failurePolicy"), "")) + } else if !supportedFailurePolicies.Has(string(*spec.FailurePolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *spec.FailurePolicy, supportedFailurePolicies.List())) + } + if spec.ParamKind != nil { + allErrors = append(allErrors, validateParamKind(*spec.ParamKind, fldPath.Child("paramKind"))...) + } + if spec.MatchConstraints == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints"), "")) + } else { + allErrors = append(allErrors, validateMatchResources(spec.MatchConstraints, fldPath.Child("matchConstraints"))...) + // at least one resourceRule must be defined to provide type information + if len(spec.MatchConstraints.ResourceRules) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints", "resourceRules"), "")) + } + } + if len(spec.Validations) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("validations"), "")) + } else { + for i, validation := range spec.Validations { + allErrors = append(allErrors, validateValidation(&validation, spec.ParamKind, fldPath.Child("validations").Index(i))...) + } + } + + return allErrors +} + +func validateParamKind(gvk admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + if len(gvk.APIVersion) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("apiVersion"), "")) + } else if gv, err := parseGroupVersion(gvk.APIVersion); err != nil { + allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gvk.APIVersion, err.Error())) + } else { + //this matches the APIService group field validation + if len(gv.Group) > 0 { + if errs := utilvalidation.IsDNS1123Subdomain(gv.Group); len(errs) > 0 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gv.Group, strings.Join(errs, ","))) + } + } + //this matches the APIService version field validation + if len(gv.Version) == 0 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gvk.APIVersion, "version must be specified")) + } else { + if errs := utilvalidation.IsDNS1035Label(gv.Version); len(errs) > 0 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gv.Version, strings.Join(errs, ","))) + } + } + } + if len(gvk.Kind) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("kind"), "")) + } else if errs := utilvalidation.IsDNS1035Label(strings.ToLower(gvk.Kind)); len(errs) > 0 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("kind"), gvk.Kind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ","))) + } + + return allErrors +} + +type groupVersion struct { + Group string + Version string +} + +// parseGroupVersion turns "group/version" string into a groupVersion struct. It reports error +// if it cannot parse the string. +func parseGroupVersion(gv string) (groupVersion, error) { + if (len(gv) == 0) || (gv == "/") { + return groupVersion{}, nil + } + + switch strings.Count(gv, "/") { + case 0: + return groupVersion{"", gv}, nil + case 1: + i := strings.Index(gv, "/") + return groupVersion{gv[:i], gv[i+1:]}, nil + default: + return groupVersion{}, fmt.Errorf("unexpected GroupVersion string: %v", gv) + } +} + +func validateMatchResources(mc *admissionregistration.MatchResources, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + if mc == nil { + return allErrors + } + if mc.MatchPolicy == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("matchPolicy"), "")) + } else if !supportedMatchPolicies.Has(string(*mc.MatchPolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *mc.MatchPolicy, supportedMatchPolicies.List())) + } + if mc.NamespaceSelector == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("namespaceSelector"), "")) + } else { + allErrors = append(allErrors, metav1validation.ValidateLabelSelector(mc.NamespaceSelector, fldPath.Child("namespaceSelector"))...) + } + + if mc.ObjectSelector == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("labelSelector"), "")) + } else { + allErrors = append(allErrors, metav1validation.ValidateLabelSelector(mc.ObjectSelector, fldPath.Child("labelSelector"))...) + } + + for i, namedRuleWithOperations := range mc.ResourceRules { + allErrors = append(allErrors, validateNamedRuleWithOperations(&namedRuleWithOperations, fldPath.Child("resourceRules").Index(i))...) + } + + for i, namedRuleWithOperations := range mc.ExcludeResourceRules { + allErrors = append(allErrors, validateNamedRuleWithOperations(&namedRuleWithOperations, fldPath.Child("excludeResourceRules").Index(i))...) + } + return allErrors +} + +func validateNamedRuleWithOperations(n *admissionregistration.NamedRuleWithOperations, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + resourceNames := sets.NewString() + for i, rName := range n.ResourceNames { + for _, msg := range path.ValidatePathSegmentName(rName, false) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("resourceNames").Index(i), rName, msg)) + } + if resourceNames.Has(rName) { + allErrors = append(allErrors, field.Duplicate(fldPath.Child("resourceNames").Index(i), rName)) + } else { + resourceNames.Insert(rName) + } + } + allErrors = append(allErrors, validateRuleWithOperations(&n.RuleWithOperations, fldPath)...) + return allErrors +} + +func validateValidation(v *admissionregistration.Validation, paramKind *admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + trimmedExpression := strings.TrimSpace(v.Expression) + trimmedMsg := strings.TrimSpace(v.Message) + if len(trimmedExpression) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified")) + } else { + result := plugincel.CompileValidatingPolicyExpression(trimmedExpression, paramKind != nil) + if result.Error != nil { + switch result.Error.Type { + case cel.ErrorTypeRequired: + allErrors = append(allErrors, field.Required(fldPath.Child("expression"), result.Error.Detail)) + case cel.ErrorTypeInvalid: + allErrors = append(allErrors, field.Invalid(fldPath.Child("expression"), v.Expression, result.Error.Detail)) + case cel.ErrorTypeInternal: + allErrors = append(allErrors, field.InternalError(fldPath.Child("expression"), result.Error)) + default: + allErrors = append(allErrors, field.InternalError(fldPath.Child("expression"), fmt.Errorf("unsupported error type: %w", result.Error))) + } + } + } + if len(v.Message) > 0 && len(trimmedMsg) == 0 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("message"), v.Message, "message must be non-empty if specified")) + } else if hasNewlines(trimmedMsg) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("message"), v.Message, "message must not contain line breaks")) + } else if hasNewlines(trimmedMsg) && trimmedMsg == "" { + allErrors = append(allErrors, field.Required(fldPath.Child("message"), "message must be specified if expression contains line breaks")) + } + if v.Reason != nil && !supportedValidationPolicyReason.Has(string(*v.Reason)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("reason"), *v.Reason, supportedValidationPolicyReason.List())) + } + return allErrors +} + +var newlineMatcher = regexp.MustCompile(`[\n\r]+`) // valid newline chars in CEL grammar +func hasNewlines(s string) bool { + return newlineMatcher.MatchString(s) +} + +// ValidateValidatingAdmissionPolicyBinding validates a ValidatingAdmissionPolicyBinding before create. +func ValidateValidatingAdmissionPolicyBinding(pb *admissionregistration.ValidatingAdmissionPolicyBinding) field.ErrorList { + return validateValidatingAdmissionPolicyBinding(pb) +} + +func validateValidatingAdmissionPolicyBinding(pb *admissionregistration.ValidatingAdmissionPolicyBinding) field.ErrorList { + allErrors := genericvalidation.ValidateObjectMeta(&pb.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) + allErrors = append(allErrors, validateValidatingAdmissionPolicyBindingSpec(&pb.Spec, field.NewPath("spec"))...) + + return allErrors +} + +func validateValidatingAdmissionPolicyBindingSpec(spec *admissionregistration.ValidatingAdmissionPolicyBindingSpec, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + + if len(spec.PolicyName) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("policyName"), "")) + } else { + for _, msg := range genericvalidation.NameIsDNSSubdomain(spec.PolicyName, false) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("policyName"), spec.PolicyName, msg)) + } + } + allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...) + allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResouces"))...) + + return allErrors +} + +func validateParamRef(pr *admissionregistration.ParamRef, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + if pr == nil { + return allErrors + } + for _, msg := range path.ValidatePathSegmentName(pr.Name, false) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("name"), pr.Name, msg)) + } + return allErrors +} + +// ValidateValidatingAdmissionPolicyUpdate validates update of validating admission policy +func ValidateValidatingAdmissionPolicyUpdate(newC, oldC *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList { + return validateValidatingAdmissionPolicy(newC) +} + +// ValidateValidatingAdmissionPolicyBindingUpdate validates update of validating admission policy +func ValidateValidatingAdmissionPolicyBindingUpdate(newC, oldC *admissionregistration.ValidatingAdmissionPolicyBinding) field.ErrorList { + return validateValidatingAdmissionPolicyBinding(newC) +} diff --git a/pkg/apis/admissionregistration/validation/validation_test.go b/pkg/apis/admissionregistration/validation/validation_test.go index 5b84ea21c3b..a673a0ac739 100644 --- a/pkg/apis/admissionregistration/validation/validation_test.go +++ b/pkg/apis/admissionregistration/validation/validation_test.go @@ -1860,3 +1860,1416 @@ func TestValidateMutatingWebhookConfigurationUpdate(t *testing.T) { } } + +func TestValidateValidatingAdmissionPolicy(t *testing.T) { + tests := []struct { + name string + config *admissionregistration.ValidatingAdmissionPolicy + expectedError string + }{ + { + name: "metadata.name validation", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!!", + }, + }, + expectedError: `metadata.name: Invalid value: "!!!!":`, + }, + { + name: "failure policy validation", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("other") + return &r + }(), + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + }, + }, + expectedError: `spec.failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, + }, + { + name: "failure policy validation", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("other") + return &r + }(), + }, + }, + expectedError: `spec.failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, + }, + { + name: "API version is required in ParamKind", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + ParamKind: &admissionregistration.ParamKind{ + Kind: "Example", + APIVersion: "test.example.com", + }, + }, + }, + expectedError: `spec.paramKind.apiVersion: Invalid value: "test.example.com"`, + }, + { + name: "API kind is required in ParamKind", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "test.example.com/v1", + }, + }, + }, + expectedError: `spec.paramKind.kind: Required value`, + }, + { + name: "API version format in ParamKind", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + ParamKind: &admissionregistration.ParamKind{ + Kind: "Example", + APIVersion: "test.example.com/!!!", + }, + }, + }, + expectedError: `pec.paramKind.apiVersion: Invalid value: "!!!":`, + }, + { + name: "API group format in ParamKind", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "!!!/v1", + Kind: "ReplicaLimit", + }, + }, + }, + expectedError: `pec.paramKind.apiVersion: Invalid value: "!!!":`, + }, + { + name: "Validations is required", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{}, + }, + + expectedError: `spec.validations: Required value`, + }, + { + name: "Invalid Validations Reason", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + Reason: func() *metav1.StatusReason { + r := metav1.StatusReason("other") + return &r + }(), + }, + }, + }, + }, + + expectedError: `spec.validations[0].reason: Unsupported value: "other"`, + }, + { + name: "MatchConstraints is required", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + }, + }, + + expectedError: `spec.matchConstraints: Required value`, + }, + { + name: "matchConstraints.resourceRules is required", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{}, + }, + }, + expectedError: `spec.matchConstraints.resourceRules: Required value`, + }, + { + name: "matchConstraints.resourceRules has at least one explicit rule", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Rule: admissionregistration.Rule{}, + }, + ResourceNames: []string{"/./."}, + }, + }, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].apiVersions: Required value`, + }, + { + name: "expression is required", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{{}}, + }, + }, + + expectedError: `spec.validations[0].expression: Required value: expression is not specified`, + }, + { + name: "matchResources resourceNames check", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + ResourceNames: []string{"/./."}, + }, + }, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resourceNames[0]: Invalid value: "/./."`, + }, + { + name: "matchResources resourceNames cannot duplicate", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + ResourceNames: []string{"test", "test"}, + }, + }, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resourceNames[1]: Duplicate value: "test"`, + }, + { + name: "matchResources validation: matchPolicy", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("other") + return &r + }(), + }, + }, + }, + expectedError: `spec.matchConstraints.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, + }, + { + name: "Operations must not be empty or nil", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + ExcludeResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].operations: Required value, spec.matchConstraints.resourceRules[1].operations: Required value, spec.matchConstraints.excludeResourceRules[0].operations: Required value, spec.matchConstraints.excludeResourceRules[1].operations: Required value`, + }, + { + name: "\"\" is NOT a valid operation", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", ""}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `Unsupported value: ""`, + }, + { + name: "operation must be either create/update/delete/connect", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"PATCH"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `Unsupported value: "PATCH"`, + }, + { + name: "wildcard operation cannot be mixed with other strings", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `if '*' is present, must not specify other operations`, + }, + { + name: `resource "*" can co-exist with resources that have subresources`, + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a/b", "a/*", "*/b"}, + }, + }, + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + }, + }, + }, + }, + { + name: `resource "*" cannot mix with resources that don't have subresources`, + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `if '*' is present, must not specify other resources without subresources`, + }, + { + name: "resource a/* cannot mix with a/x", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a/x"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + }, + { + name: "resource a/* can mix with a", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "resource */a cannot mix with x/a", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/a", "x/a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + }, + { + name: "resource */* cannot mix with other resources", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*", "a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + }, + { + name: "invalid expression", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x in [1, 2, ", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.validations[0].expression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: :1:19: Syntax error: missing ']' at '`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateValidatingAdmissionPolicy(test.config) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} + +func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) { + tests := []struct { + name string + oldconfig *admissionregistration.ValidatingAdmissionPolicy + config *admissionregistration.ValidatingAdmissionPolicy + expectedError string + }{ + { + name: "should pass on valid new ValidatingAdmissionPolicy", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + oldconfig: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "should pass on valid new ValidatingAdmissionPolicy with invalid old ValidatingAdmissionPolicy", + config: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + Validations: []admissionregistration.Validation{ + { + Expression: "object.x < 100", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + oldconfig: &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateValidatingAdmissionPolicyUpdate(test.config, test.oldconfig) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} + +func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { + tests := []struct { + name string + config *admissionregistration.ValidatingAdmissionPolicyBinding + expectedError string + }{ + { + name: "metadata.name validation", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!!", + }, + }, + expectedError: `metadata.name: Invalid value: "!!!!":`, + }, + { + name: "PolicyName is required", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{}, + }, + expectedError: `spec.policyName: Required value`, + }, + { + name: "matchResources validation: matchPolicy", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("other") + return &r + }(), + }, + }, + }, + expectedError: `spec.matchResouces.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, + }, + { + name: "Operations must not be empty or nil", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + ExcludeResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.matchResouces.resourceRules[0].operations: Required value, spec.matchResouces.resourceRules[1].operations: Required value, spec.matchResouces.excludeResourceRules[0].operations: Required value, spec.matchResouces.excludeResourceRules[1].operations: Required value`, + }, + { + name: "\"\" is NOT a valid operation", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", ""}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `Unsupported value: ""`, + }, + { + name: "operation must be either create/update/delete/connect", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"PATCH"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `Unsupported value: "PATCH"`, + }, + { + name: "wildcard operation cannot be mixed with other strings", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `if '*' is present, must not specify other operations`, + }, + { + name: `resource "*" can co-exist with resources that have subresources`, + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a/b", "a/*", "*/b"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: `resource "*" cannot mix with resources that don't have subresources`, + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `if '*' is present, must not specify other resources without subresources`, + }, + { + name: "resource a/* cannot mix with a/x", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a/x"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.matchResouces.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + }, + { + name: "resource a/* can mix with a", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "resource */a cannot mix with x/a", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/a", "x/a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.matchResouces.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + }, + { + name: "resource */* cannot mix with other resources", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*", "a"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: `spec.matchResouces.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateValidatingAdmissionPolicyBinding(test.config) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} + +func TestValidateValidatingAdmissionPolicyBindingUpdate(t *testing.T) { + tests := []struct { + name string + oldconfig *admissionregistration.ValidatingAdmissionPolicyBinding + config *admissionregistration.ValidatingAdmissionPolicyBinding + expectedError string + }{ + { + name: "should pass on valid new ValidatingAdmissionPolicyBinding", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + oldconfig: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "should pass on valid new ValidatingAdmissionPolicyBinding with invalid old ValidatingAdmissionPolicyBinding", + config: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + }, + oldconfig: &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateValidatingAdmissionPolicyBindingUpdate(test.config, test.oldconfig) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} diff --git a/pkg/controlplane/instance.go b/pkg/controlplane/instance.go index 3dd15542957..a31efd0e6bf 100644 --- a/pkg/controlplane/instance.go +++ b/pkg/controlplane/instance.go @@ -26,6 +26,7 @@ import ( "time" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" apiserverinternalv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1" appsv1 "k8s.io/api/apps/v1" authenticationv1 "k8s.io/api/authentication/v1" @@ -668,6 +669,7 @@ var ( // alphaAPIGroupVersionsDisabledByDefault holds the alpha APIs we have. They are always disabled by default. alphaAPIGroupVersionsDisabledByDefault = []schema.GroupVersion{ + admissionregistrationv1alpha1.SchemeGroupVersion, apiserverinternalv1alpha1.SchemeGroupVersion, authenticationv1alpha1.SchemeGroupVersion, networkingapiv1alpha1.SchemeGroupVersion, diff --git a/pkg/kubeapiserver/default_storage_factory_builder.go b/pkg/kubeapiserver/default_storage_factory_builder.go index fb66d0c1f32..6a9cb5b7145 100644 --- a/pkg/kubeapiserver/default_storage_factory_builder.go +++ b/pkg/kubeapiserver/default_storage_factory_builder.go @@ -26,6 +26,7 @@ import ( serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/apis/apps" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/events" @@ -70,6 +71,8 @@ func NewStorageFactoryConfig() *StorageFactoryConfig { // TODO (https://github.com/kubernetes/kubernetes/issues/108451): remove the override in 1.25. // apisstorage.Resource("csistoragecapacities").WithVersion("v1beta1"), networking.Resource("clustercidrs").WithVersion("v1alpha1"), + admissionregistration.Resource("validatingadmissionpolicies").WithVersion("v1alpha1"), + admissionregistration.Resource("validatingadmissionpolicybindings").WithVersion("v1alpha1"), } return &StorageFactoryConfig{ diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 78827cbdd8e..5ef6a8b50d8 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -553,6 +553,24 @@ func AddHandlers(h printers.PrintHandler) { h.TableHandler(validatingWebhookColumnDefinitions, printValidatingWebhook) h.TableHandler(validatingWebhookColumnDefinitions, printValidatingWebhookList) + validatingAdmissionPolicy := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "Validations", Type: "integer", Description: "Validations indicates the number of validation rules defined in this configuration"}, + {Name: "ParamKind", Type: "string", Description: "ParamKind specifies the kind of resources used to parameterize this policy"}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + h.TableHandler(validatingAdmissionPolicy, printValidatingAdmissionPolicy) + h.TableHandler(validatingAdmissionPolicy, printValidatingAdmissionPolicyList) + + validatingAdmissionPolicyBinding := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "PolicyName", Type: "string", Description: "PolicyName indicates the policy definition which the policy binding binded to"}, + {Name: "ParamRef", Type: "string", Description: "ParamRef indicates the param resource which sets the configration param"}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + h.TableHandler(validatingAdmissionPolicyBinding, printValidatingAdmissionPolicyBinding) + h.TableHandler(validatingAdmissionPolicyBinding, printValidatingAdmissionPolicyBindingList) + flowSchemaColumnDefinitions := []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, {Name: "PriorityLevel", Type: "string", Description: flowcontrolv1beta3.PriorityLevelConfigurationReference{}.SwaggerDoc()["name"]}, @@ -1533,6 +1551,59 @@ func printValidatingWebhookList(list *admissionregistration.ValidatingWebhookCon return rows, nil } +func printValidatingAdmissionPolicy(obj *admissionregistration.ValidatingAdmissionPolicy, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + paramKind := "" + if obj.Spec.ParamKind != nil { + paramKind = obj.Spec.ParamKind.APIVersion + "/" + obj.Spec.ParamKind.Kind + } + row.Cells = append(row.Cells, obj.Name, int64(len(obj.Spec.Validations)), paramKind, translateTimestampSince(obj.CreationTimestamp)) + return []metav1.TableRow{row}, nil +} + +func printValidatingAdmissionPolicyList(list *admissionregistration.ValidatingAdmissionPolicyList, options printers.GenerateOptions) ([]metav1.TableRow, error) { + rows := make([]metav1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printValidatingAdmissionPolicy(&list.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + +func printValidatingAdmissionPolicyBinding(obj *admissionregistration.ValidatingAdmissionPolicyBinding, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + paramName := "" + if obj.Spec.ParamRef != nil { + if obj.Spec.ParamRef.Namespace != "" { + paramName = obj.Spec.ParamRef.Namespace + "/" + obj.Spec.ParamRef.Name + } else { + paramName = obj.Spec.ParamRef.Name + } + + } + row.Cells = append(row.Cells, obj.Name, obj.Spec.PolicyName, paramName, translateTimestampSince(obj.CreationTimestamp)) + return []metav1.TableRow{row}, nil +} + +func printValidatingAdmissionPolicyBindingList(list *admissionregistration.ValidatingAdmissionPolicyBindingList, options printers.GenerateOptions) ([]metav1.TableRow, error) { + rows := make([]metav1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printValidatingAdmissionPolicyBinding(&list.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + func printNamespace(obj *api.Namespace, options printers.GenerateOptions) ([]metav1.TableRow, error) { row := metav1.TableRow{ Object: runtime.RawExtension{Object: obj}, diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index 941cb747726..31c5ea22ba0 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -5983,6 +5983,18 @@ func TestTableRowDeepCopyShouldNotPanic(t *testing.T) { return printValidatingWebhook(&admissionregistration.ValidatingWebhookConfiguration{}, printers.GenerateOptions{}) }, }, + { + name: "ValidatingAdmissionPolicy", + printer: func() ([]metav1.TableRow, error) { + return printValidatingAdmissionPolicy(&admissionregistration.ValidatingAdmissionPolicy{}, printers.GenerateOptions{}) + }, + }, + { + name: "ValidatingAdmissionPolicyBinding", + printer: func() ([]metav1.TableRow, error) { + return printValidatingAdmissionPolicyBinding(&admissionregistration.ValidatingAdmissionPolicyBinding{}, printers.GenerateOptions{}) + }, + }, { name: "Namespace", printer: func() ([]metav1.TableRow, error) { diff --git a/pkg/registry/admissionregistration/rest/storage_apiserver.go b/pkg/registry/admissionregistration/rest/storage_apiserver.go index 0e03b7186dc..7ea72f6e973 100644 --- a/pkg/registry/admissionregistration/rest/storage_apiserver.go +++ b/pkg/registry/admissionregistration/rest/storage_apiserver.go @@ -18,6 +18,7 @@ package rest import ( admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" @@ -25,6 +26,8 @@ import ( "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/admissionregistration" mutatingwebhookconfigurationstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingwebhookconfiguration/storage" + validatingadmissionpolicystorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicy/storage" + policybindingstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage" validatingwebhookconfigurationstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingwebhookconfiguration/storage" ) @@ -40,6 +43,12 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag } else if len(storageMap) > 0 { apiGroupInfo.VersionedResourcesStorageMap[admissionregistrationv1.SchemeGroupVersion.Version] = storageMap } + + if storageMap, err := p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter); err != nil { + return genericapiserver.APIGroupInfo{}, err + } else if len(storageMap) > 0 { + apiGroupInfo.VersionedResourcesStorageMap[admissionregistrationv1alpha1.SchemeGroupVersion.Version] = storageMap + } return apiGroupInfo, nil } @@ -67,6 +76,30 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.API return storage, nil } +func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) { + storage := map[string]rest.Storage{} + + // validatingadmissionpolicies + if resource := "validatingadmissionpolicies"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) { + policyStorage, err := validatingadmissionpolicystorage.NewREST(restOptionsGetter) + if err != nil { + return storage, err + } + storage[resource] = policyStorage + } + + // validatingadmissionpolicybindings + if resource := "validatingadmissionpolicybindings"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) { + policyBindingStorage, err := policybindingstorage.NewREST(restOptionsGetter) + if err != nil { + return storage, err + } + storage[resource] = policyBindingStorage + } + + return storage, nil +} + func (p RESTStorageProvider) GroupName() string { return admissionregistration.GroupName } diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/doc.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/doc.go new file mode 100644 index 00000000000..217ee93f469 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/doc.go @@ -0,0 +1,17 @@ +/* +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 validatingadmissionpolicy // import "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicy" diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage.go new file mode 100644 index 00000000000..6985ad96e4e --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage.go @@ -0,0 +1,65 @@ +/* +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 storage + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" + "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicy" +) + +// REST implements a RESTStorage for validatingAdmissionPolicy against etcd +type REST struct { + *genericregistry.Store +} + +// NewREST returns a RESTStorage object that will work against validatingAdmissionPolicy. +func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) { + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &admissionregistration.ValidatingAdmissionPolicy{} }, + NewListFunc: func() runtime.Object { return &admissionregistration.ValidatingAdmissionPolicyList{} }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*admissionregistration.ValidatingAdmissionPolicy).Name, nil + }, + DefaultQualifiedResource: admissionregistration.Resource("validatingadmissionpolicies"), + + CreateStrategy: validatingadmissionpolicy.Strategy, + UpdateStrategy: validatingadmissionpolicy.Strategy, + DeleteStrategy: validatingadmissionpolicy.Strategy, + + TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + return &REST{store}, nil +} + +// Implement CategoriesProvider +var _ rest.CategoriesProvider = &REST{} + +// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of. +func (r *REST) Categories() []string { + return []string{"api-extensions"} +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go new file mode 100644 index 00000000000..1f1bfceb5db --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go @@ -0,0 +1,221 @@ +/* +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 storage + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" + etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/registrytest" + + // Ensure that admissionregistration package is initialized. + _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" +) + +func TestCreate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + configuration := validValidatingAdmissionPolicy() + test.TestCreate( + // valid + configuration, + // invalid + newValidatingAdmissionPolicy(""), + ) +} + +func TestUpdate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + + test.TestUpdate( + // valid + validValidatingAdmissionPolicy(), + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.ValidatingAdmissionPolicy) + object.Labels = map[string]string{"c": "d"} + return object + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.ValidatingAdmissionPolicy) + object.Name = "" + return object + }, + ) +} + +func TestGet(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestGet(validValidatingAdmissionPolicy()) +} + +func TestList(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestList(validValidatingAdmissionPolicy()) +} + +func TestDelete(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestDelete(validValidatingAdmissionPolicy()) +} + +func TestWatch(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + validValidatingAdmissionPolicy(), + []labels.Set{}, + []labels.Set{ + {"hoo": "bar"}, + }, + []fields.Set{ + {"metadata.name": "foo"}, + }, + []fields.Set{ + {"metadata.name": "nomatch"}, + }, + ) +} + +func validValidatingAdmissionPolicy() *admissionregistration.ValidatingAdmissionPolicy { + return &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "rules.example.com/v1", + Kind: "ReplicaLimit", + }, + Validations: []admissionregistration.Validation{ + { + Expression: "object.spec.replicas <= params.maxReplicas", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + }, + }, + } +} + +func newValidatingAdmissionPolicy(name string) *admissionregistration.ValidatingAdmissionPolicy { + ignore := admissionregistration.Ignore + return &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"foo": "bar"}, + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "rules.example.com/v1", + Kind: "ReplicaLimit", + }, + Validations: []admissionregistration.Validation{ + { + Expression: "object.spec.replicas <= params.maxReplicas", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + FailurePolicy: &ignore, + }, + } +} + +func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("validatingadmissionpolicies")) + restOptions := generic.RESTOptions{ + StorageConfig: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1, + ResourcePrefix: "validatingadmissionpolicies"} + storage, err := NewREST(restOptions) + if err != nil { + t.Fatalf("unexpected error from REST storage: %v", err) + } + return storage, server +} + +func TestCategories(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + expected := []string{"api-extensions"} + registrytest.AssertCategories(t, storage, expected) +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy.go new file mode 100644 index 00000000000..f12ecca4fe5 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy.go @@ -0,0 +1,96 @@ +/* +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 validatingadmissionpolicy + +import ( + "context" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/apis/admissionregistration/validation" +) + +// validatingAdmissionPolicyStrategy implements verification logic for ValidatingAdmissionPolicy. +type validatingAdmissionPolicyStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating validatingAdmissionPolicy objects. +var Strategy = validatingAdmissionPolicyStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} + +// NamespaceScoped returns false because ValidatingAdmissionPolicy is cluster-scoped resource. +func (validatingAdmissionPolicyStrategy) NamespaceScoped() bool { + return false +} + +// PrepareForCreate clears the status of an validatingAdmissionPolicy before creation. +func (validatingAdmissionPolicyStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + ic := obj.(*admissionregistration.ValidatingAdmissionPolicy) + ic.Generation = 1 +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (validatingAdmissionPolicyStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newIC := obj.(*admissionregistration.ValidatingAdmissionPolicy) + oldIC := old.(*admissionregistration.ValidatingAdmissionPolicy) + + // Any changes to the spec increment the generation number, any changes to the + // status should reflect the generation number of the corresponding object. + // See metav1.ObjectMeta description for more information on Generation. + if !apiequality.Semantic.DeepEqual(oldIC.Spec, newIC.Spec) { + newIC.Generation = oldIC.Generation + 1 + } +} + +// Validate validates a new validatingAdmissionPolicy. +func (validatingAdmissionPolicyStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + return validation.ValidateValidatingAdmissionPolicy(obj.(*admissionregistration.ValidatingAdmissionPolicy)) +} + +// WarningsOnCreate returns warnings for the creation of the given object. +func (validatingAdmissionPolicyStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} + +// Canonicalize normalizes the object after validation. +func (validatingAdmissionPolicyStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is true for validatingAdmissionPolicy; this means you may create one with a PUT request. +func (validatingAdmissionPolicyStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (validatingAdmissionPolicyStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateValidatingAdmissionPolicyUpdate(obj.(*admissionregistration.ValidatingAdmissionPolicy), old.(*admissionregistration.ValidatingAdmissionPolicy)) +} + +// WarningsOnUpdate returns warnings for the given update. +func (validatingAdmissionPolicyStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + +// AllowUnconditionalUpdate is the default update policy for validatingAdmissionPolicy objects. Status update should +// only be allowed if version match. +func (validatingAdmissionPolicyStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go new file mode 100644 index 00000000000..c39008a8703 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go @@ -0,0 +1,94 @@ +/* +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 validatingadmissionpolicy + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +func TestValidatingAdmissionPolicyStrategy(t *testing.T) { + ctx := genericapirequest.NewDefaultContext() + if Strategy.NamespaceScoped() { + t.Error("ValidatingAdmissionPolicy strategy must be cluster scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("ValidatingAdmissionPolicy should not allow create on update") + } + + configuration := validValidatingAdmissionPolicy() + Strategy.PrepareForCreate(ctx, configuration) + errs := Strategy.Validate(ctx, configuration) + if len(errs) != 0 { + t.Errorf("Unexpected error validating %v", errs) + } + invalidConfiguration := &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: ""}, + } + Strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration) + errs = Strategy.ValidateUpdate(ctx, invalidConfiguration, configuration) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } +} +func validValidatingAdmissionPolicy() *admissionregistration.ValidatingAdmissionPolicy { + ignore := admissionregistration.Ignore + return &admissionregistration.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.ValidatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + Kind: "ReplicaLimit", + APIVersion: "rules.example.com/v1", + }, + Validations: []admissionregistration.Validation{ + { + Expression: "object.spec.replicas <= params.maxReplicas", + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + FailurePolicy: &ignore, + }, + } +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/doc.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/doc.go new file mode 100644 index 00000000000..fac83d6a1ac --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/doc.go @@ -0,0 +1,17 @@ +/* +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 validatingadmissionpolicybinding // import "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicybinding" diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage.go new file mode 100644 index 00000000000..97773cc86ac --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage.go @@ -0,0 +1,65 @@ +/* +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 storage + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" + "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicybinding" +) + +// REST implements a RESTStorage for policyBinding against etcd +type REST struct { + *genericregistry.Store +} + +// NewREST returns a RESTStorage object that will work against policyBinding. +func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) { + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &admissionregistration.ValidatingAdmissionPolicyBinding{} }, + NewListFunc: func() runtime.Object { return &admissionregistration.ValidatingAdmissionPolicyBindingList{} }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*admissionregistration.ValidatingAdmissionPolicyBinding).Name, nil + }, + DefaultQualifiedResource: admissionregistration.Resource("validatingadmissionpolicybindings"), + + CreateStrategy: validatingadmissionpolicybinding.Strategy, + UpdateStrategy: validatingadmissionpolicybinding.Strategy, + DeleteStrategy: validatingadmissionpolicybinding.Strategy, + + TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + return &REST{store}, nil +} + +// Implement CategoriesProvider +var _ rest.CategoriesProvider = &REST{} + +// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of. +func (r *REST) Categories() []string { + return []string{"api-extensions"} +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go new file mode 100644 index 00000000000..8cf94d04c35 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go @@ -0,0 +1,192 @@ +/* +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 storage + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" + etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/registrytest" + + // Ensure that admissionregistration package is initialized. + _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" +) + +func TestCreate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + configuration := validPolicyBinding() + test.TestCreate( + // valid + configuration, + // invalid + newPolicyBinding(""), + ) +} + +func TestUpdate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + + test.TestUpdate( + // valid + validPolicyBinding(), + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.ValidatingAdmissionPolicyBinding) + object.Labels = map[string]string{"c": "d"} + return object + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.ValidatingAdmissionPolicyBinding) + object.Name = "" + return object + }, + ) +} + +func TestGet(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestGet(validPolicyBinding()) +} + +func TestList(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestList(validPolicyBinding()) +} + +func TestDelete(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestDelete(validPolicyBinding()) +} + +func TestWatch(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + validPolicyBinding(), + []labels.Set{}, + []labels.Set{ + {"hoo": "bar"}, + }, + []fields.Set{ + {"metadata.name": "foo"}, + }, + []fields.Set{ + {"metadata.name": "nomatch"}, + }, + ) +} + +func validPolicyBinding() *admissionregistration.ValidatingAdmissionPolicyBinding { + return &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "param-test", + }, + MatchResources: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + }, + } +} + +func newPolicyBinding(name string) *admissionregistration.ValidatingAdmissionPolicyBinding { + return &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"foo": "bar"}, + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "param-test", + }, + MatchResources: &admissionregistration.MatchResources{}, + }, + } +} + +func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("validatingadmissionpolicybindings")) + restOptions := generic.RESTOptions{ + StorageConfig: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1, + ResourcePrefix: "validatingadmissionpolicybindings"} + storage, err := NewREST(restOptions) + if err != nil { + t.Fatalf("unexpected error from REST storage: %v", err) + } + return storage, server +} + +func TestCategories(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + expected := []string{"api-extensions"} + registrytest.AssertCategories(t, storage, expected) +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy.go new file mode 100644 index 00000000000..8fef843e6c6 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy.go @@ -0,0 +1,96 @@ +/* +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 validatingadmissionpolicybinding + +import ( + "context" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/apis/admissionregistration/validation" +) + +// ValidatingAdmissionPolicyBindingStrategy implements verification logic for ValidatingAdmissionPolicyBinding. +type ValidatingAdmissionPolicyBindingStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating ValidatingAdmissionPolicyBinding objects. +var Strategy = ValidatingAdmissionPolicyBindingStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} + +// NamespaceScoped returns false because ValidatingAdmissionPolicyBinding is cluster-scoped resource. +func (ValidatingAdmissionPolicyBindingStrategy) NamespaceScoped() bool { + return false +} + +// PrepareForCreate clears the status of an ValidatingAdmissionPolicyBinding before creation. +func (ValidatingAdmissionPolicyBindingStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + ic := obj.(*admissionregistration.ValidatingAdmissionPolicyBinding) + ic.Generation = 1 +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (ValidatingAdmissionPolicyBindingStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newIC := obj.(*admissionregistration.ValidatingAdmissionPolicyBinding) + oldIC := old.(*admissionregistration.ValidatingAdmissionPolicyBinding) + + // Any changes to the spec increment the generation number, any changes to the + // status should reflect the generation number of the corresponding object. + // See metav1.ObjectMeta description for more information on Generation. + if !apiequality.Semantic.DeepEqual(oldIC.Spec, newIC.Spec) { + newIC.Generation = oldIC.Generation + 1 + } +} + +// Validate validates a new ValidatingAdmissionPolicyBinding. +func (ValidatingAdmissionPolicyBindingStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + return validation.ValidateValidatingAdmissionPolicyBinding(obj.(*admissionregistration.ValidatingAdmissionPolicyBinding)) +} + +// WarningsOnCreate returns warnings for the creation of the given object. +func (ValidatingAdmissionPolicyBindingStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} + +// Canonicalize normalizes the object after validation. +func (ValidatingAdmissionPolicyBindingStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is true for ValidatingAdmissionPolicyBinding; this means you may create one with a PUT request. +func (ValidatingAdmissionPolicyBindingStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (ValidatingAdmissionPolicyBindingStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateValidatingAdmissionPolicyBindingUpdate(obj.(*admissionregistration.ValidatingAdmissionPolicyBinding), old.(*admissionregistration.ValidatingAdmissionPolicyBinding)) +} + +// WarningsOnUpdate returns warnings for the given update. +func (ValidatingAdmissionPolicyBindingStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + +// AllowUnconditionalUpdate is the default update policy for ValidatingAdmissionPolicyBinding objects. Status update should +// only be allowed if version match. +func (ValidatingAdmissionPolicyBindingStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go new file mode 100644 index 00000000000..415932962b8 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go @@ -0,0 +1,63 @@ +/* +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 validatingadmissionpolicybinding + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +func TestPolicyBindingStrategy(t *testing.T) { + ctx := genericapirequest.NewDefaultContext() + if Strategy.NamespaceScoped() { + t.Error("PolicyBinding strategy must be cluster scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("PolicyBinding should not allow create on update") + } + + configuration := validPolicyBinding() + Strategy.PrepareForCreate(ctx, configuration) + errs := Strategy.Validate(ctx, configuration) + if len(errs) != 0 { + t.Errorf("Unexpected error validating %v", errs) + } + invalidConfiguration := &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{Name: ""}, + } + Strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration) + errs = Strategy.ValidateUpdate(ctx, invalidConfiguration, configuration) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } +} +func validPolicyBinding() *admissionregistration.ValidatingAdmissionPolicyBinding { + return &admissionregistration.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + }, + }, + } +} diff --git a/staging/src/k8s.io/api/admissionregistration/v1/types.go b/staging/src/k8s.io/api/admissionregistration/v1/types.go index 29873b796bb..e74b276f654 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1/types.go +++ b/staging/src/k8s.io/api/admissionregistration/v1/types.go @@ -26,11 +26,13 @@ type Rule struct { // APIGroups is the API groups the resources belong to. '*' is all groups. // If '*' is present, the length of the slice must be one. // Required. + // +listType=atomic APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,1,rep,name=apiGroups"` // APIVersions is the API versions the resources belong to. '*' is all versions. // If '*' is present, the length of the slice must be one. // Required. + // +listType=atomic APIVersions []string `json:"apiVersions,omitempty" protobuf:"bytes,2,rep,name=apiVersions"` // Resources is a list of resources this rule applies to. @@ -48,6 +50,7 @@ type Rule struct { // // Depending on the enclosing object, subresources might not be allowed. // Required. + // +listType=atomic Resources []string `json:"resources,omitempty" protobuf:"bytes,3,rep,name=resources"` // scope specifies the scope of this rule. @@ -474,6 +477,7 @@ type RuleWithOperations struct { // for all of those operations and any future admission operations that are added. // If '*' is present, the length of the slice must be one. // Required. + // +listType=atomic Operations []OperationType `json:"operations,omitempty" protobuf:"bytes,1,rep,name=operations,casttype=OperationType"` // Rule is embedded, it describes other criteria of the rule, like // APIGroups, APIVersions, Resources, etc. diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/doc.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/doc.go index 55546aa06eb..385c60e0d3f 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/doc.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/doc.go @@ -20,7 +20,4 @@ limitations under the License. // +groupName=admissionregistration.k8s.io // Package v1alpha1 is the v1alpha1 version of the API. -// AdmissionConfiguration and AdmissionPluginConfiguration are legacy static admission plugin configuration -// MutatingWebhookConfiguration and ValidatingWebhookConfiguration are for the -// new dynamic admission controller configuration. package v1alpha1 // import "k8s.io/api/admissionregistration/v1alpha1" diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go index 9a06eece52a..a6225bdfae2 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go @@ -50,6 +50,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ValidatingWebhookConfigurationList{}, &MutatingWebhookConfiguration{}, &MutatingWebhookConfigurationList{}, + &ValidatingAdmissionPolicy{}, + &ValidatingAdmissionPolicyList{}, + &ValidatingAdmissionPolicyBinding{}, + &ValidatingAdmissionPolicyBindingList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go index aff789d6dfa..b64bc628f71 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go @@ -17,64 +17,26 @@ limitations under the License. package v1alpha1 import ( + v1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Rule is a tuple of APIGroups, APIVersion, and Resources.It is recommended // to make sure that all the tuple expansions are valid. -type Rule struct { - // APIGroups is the API groups the resources belong to. '*' is all groups. - // If '*' is present, the length of the slice must be one. - // Required. - APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,1,rep,name=apiGroups"` - - // APIVersions is the API versions the resources belong to. '*' is all versions. - // If '*' is present, the length of the slice must be one. - // Required. - APIVersions []string `json:"apiVersions,omitempty" protobuf:"bytes,2,rep,name=apiVersions"` - - // Resources is a list of resources this rule applies to. - // - // For example: - // 'pods' means pods. - // 'pods/log' means the log subresource of pods. - // '*' means all resources, but not subresources. - // 'pods/*' means all subresources of pods. - // '*/scale' means all scale subresources. - // '*/*' means all resources and their subresources. - // - // If wildcard is present, the validation rule will ensure resources do not - // overlap with each other. - // - // Depending on the enclosing object, subresources might not be allowed. - // Required. - Resources []string `json:"resources,omitempty" protobuf:"bytes,3,rep,name=resources"` - - // scope specifies the scope of this rule. - // Valid values are "Cluster", "Namespaced", and "*" - // "Cluster" means that only cluster-scoped resources will match this rule. - // Namespace API objects are cluster-scoped. - // "Namespaced" means that only namespaced resources will match this rule. - // "*" means that there are no scope restrictions. - // Subresources match the scope of their parent resource. - // Default is "*". - // - // +optional - Scope *ScopeType `json:"scope,omitempty" protobuf:"bytes,4,rep,name=scope"` -} +type Rule = v1.Rule // ScopeType specifies a scope for a Rule. // +enum -type ScopeType string +type ScopeType = v1.ScopeType const ( // ClusterScope means that scope is limited to cluster-scoped objects. // Namespace objects are cluster-scoped. - ClusterScope ScopeType = "Cluster" + ClusterScope ScopeType = v1.ClusterScope // NamespacedScope means that scope is limited to namespaced objects. - NamespacedScope ScopeType = "Namespaced" + NamespacedScope ScopeType = v1.NamespacedScope // AllScopes means that all scopes are included. - AllScopes ScopeType = "*" + AllScopes ScopeType = v1.AllScopes ) // FailurePolicyType specifies a failure policy that defines how unrecognized errors from the admission endpoint are handled. @@ -99,132 +61,208 @@ const ( Equivalent MatchPolicyType = "Equivalent" ) -// SideEffectClass specifies the types of side effects a webhook may have. -// +enum -type SideEffectClass string - -const ( - // SideEffectClassUnknown means that no information is known about the side effects of calling the webhook. - // If a request with the dry-run attribute would trigger a call to this webhook, the request will instead fail. - SideEffectClassUnknown SideEffectClass = "Unknown" - // SideEffectClassNone means that calling the webhook will have no side effects. - SideEffectClassNone SideEffectClass = "None" - // SideEffectClassSome means that calling the webhook will possibly have side effects. - // If a request with the dry-run attribute would trigger a call to this webhook, the request will instead fail. - SideEffectClassSome SideEffectClass = "Some" - // SideEffectClassNoneOnDryRun means that calling the webhook will possibly have side effects, but if the - // request being reviewed has the dry-run attribute, the side effects will be suppressed. - SideEffectClassNoneOnDryRun SideEffectClass = "NoneOnDryRun" -) - // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.26 -// ValidatingWebhookConfiguration describes the configuration of and admission webhook that accept or reject and object without changing it. -type ValidatingWebhookConfiguration struct { +// ValidatingAdmissionPolicy describes the definition of an admission validation policy that accepts or rejects an object without changing it. +type ValidatingAdmissionPolicy struct { metav1.TypeMeta `json:",inline"` // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. // +optional metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - // Webhooks is a list of webhooks and the affected resources and operations. - // +optional - // +patchMergeKey=name - // +patchStrategy=merge - Webhooks []ValidatingWebhook `json:"webhooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=Webhooks"` + // Specification of the desired behavior of the ValidatingAdmissionPolicy. + Spec ValidatingAdmissionPolicySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.26 -// ValidatingWebhookConfigurationList is a list of ValidatingWebhookConfiguration. -type ValidatingWebhookConfigurationList struct { +// ValidatingAdmissionPolicyList is a list of ValidatingAdmissionPolicy. +type ValidatingAdmissionPolicyList struct { metav1.TypeMeta `json:",inline"` // Standard list metadata. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds // +optional metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - // List of ValidatingWebhookConfiguration. - Items []ValidatingWebhookConfiguration `json:"items" protobuf:"bytes,2,rep,name=items"` + // List of ValidatingAdmissionPolicy. + Items []ValidatingAdmissionPolicy `json:"items,omitempty" protobuf:"bytes,2,rep,name=items"` } -// +genclient -// +genclient:nonNamespaced -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// MutatingWebhookConfiguration describes the configuration of and admission webhook that accept or reject and may change the object. -type MutatingWebhookConfiguration struct { - metav1.TypeMeta `json:",inline"` - // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. +// ValidatingAdmissionPolicySpec is the specification of the desired behavior of the AdmissionPolicy. +type ValidatingAdmissionPolicySpec struct { + // ParamKind specifies the kind of resources used to parameterize this policy. + // If absent, there are no parameters for this policy and the param CEL variable will not be provided to validation expressions. + // If ParamKind refers to a non-existent kind, this policy definition is mis-configured and the FailurePolicy is applied. + // If paramKind is specified but paramRef is unset in ValidatingAdmissionPolicyBinding, the params variable will be null. // +optional - metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - // Webhooks is a list of webhooks and the affected resources and operations. - // +optional - // +patchMergeKey=name - // +patchStrategy=merge - Webhooks []MutatingWebhook `json:"webhooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=Webhooks"` -} + ParamKind *ParamKind `json:"paramKind,omitempty" protobuf:"bytes,1,rep,name=paramKind"` -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// MutatingWebhookConfigurationList is a list of MutatingWebhookConfiguration. -type MutatingWebhookConfigurationList struct { - metav1.TypeMeta `json:",inline"` - // Standard list metadata. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - // +optional - metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - // List of MutatingWebhookConfiguration. - Items []MutatingWebhookConfiguration `json:"items" protobuf:"bytes,2,rep,name=items"` -} - -// ValidatingWebhook describes an admission webhook and the resources and operations it applies to. -type ValidatingWebhook struct { - // The name of the admission webhook. - // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where - // "imagepolicy" is the name of the webhook, and kubernetes.io is the name - // of the organization. + // MatchConstraints specifies what resources this policy is designed to validate. + // The AdmissionPolicy cares about a request if it matches _all_ Constraints. + // However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API + // ValidatingAdmissionPolicy cannot match ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding. // Required. - Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + MatchConstraints *MatchResources `json:"matchConstraints,omitempty" protobuf:"bytes,2,rep,name=matchConstraints"` - // ClientConfig defines how to communicate with the hook. - // Required - ClientConfig WebhookClientConfig `json:"clientConfig" protobuf:"bytes,2,opt,name=clientConfig"` + // Validations contain CEL expressions which is used to apply the validation. + // A minimum of one validation is required for a policy definition. + // +listType=atomic + // Required. + Validations []Validation `json:"validations" protobuf:"bytes,3,rep,name=validations"` - // Rules describes what operations on what resources/subresources the webhook cares about. - // The webhook cares about an operation if it matches _any_ Rule. - // However, in order to prevent ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks - // from putting the cluster in a state which cannot be recovered from without completely - // disabling the plugin, ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks are never called - // on admission requests for ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects. - Rules []RuleWithOperations `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"` - - // FailurePolicy defines how unrecognized errors from the admission endpoint are handled - - // allowed values are Ignore or Fail. Defaults to Fail. + // FailurePolicy defines how to handle failures for the admission policy. + // Failures can occur from invalid or mis-configured policy definitions or bindings. + // A policy is invalid if spec.paramKind refers to a non-existent Kind. + // A binding is invalid if spec.paramRef.name refers to a non-existent resource. + // Allowed values are Ignore or Fail. Defaults to Fail. // +optional FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"` +} - // matchPolicy defines how the "rules" list is used to match incoming requests. - // Allowed values are "Exact" or "Equivalent". +// ParamKind is a tuple of Group Kind and Version. +// +structType=atomic +type ParamKind struct { + // APIVersion is the API group version the resources belong to. + // In format of "group/version". + // Required. + APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,1,rep,name=apiVersion"` + + // Kind is the API kind the resources belong to. + // Required. + Kind string `json:"kind,omitempty" protobuf:"bytes,2,rep,name=kind"` +} + +// Validation specifies the CEL expression which is used to apply the validation. +type Validation struct { + // Expression represents the expression which will be evaluated by CEL. + // ref: https://github.com/google/cel-spec + // CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables: // - // - Exact: match a request only if it exactly matches a specified rule. - // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, - // but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, - // a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the webhook. + //'object' - The object from the incoming request. The value is null for DELETE requests. + //'oldObject' - The existing object. The value is null for CREATE requests. + //'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + //'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. // - // - Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. - // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, - // and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, - // a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the webhook. + // The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the + // object. No other metadata properties are accessible. // - // Defaults to "Equivalent" + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Accessible property names are escaped according to the following rules when accessed in the expression: + // - '__' escapes to '__underscores__' + // - '.' escapes to '__dot__' + // - '-' escapes to '__dash__' + // - '/' escapes to '__slash__' + // - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are: + // "true", "false", "null", "in", "as", "break", "const", "continue", "else", "for", "function", "if", + // "import", "let", "loop", "package", "namespace", "return". + // Examples: + // - Expression accessing a property named "namespace": {"Expression": "object.__namespace__ > 0"} + // - Expression accessing a property named "x-prop": {"Expression": "object.x__dash__prop > 0"} + // - Expression accessing a property named "redact__d": {"Expression": "object.redact__underscores__d > 0"} + // + // Equality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. + // Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type: + // - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and + // non-intersecting elements in `Y` are appended, retaining their partial order. + // - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values + // are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with + // non-intersecting keys are appended, retaining their partial order. + // Required. + Expression string `json:"expression" protobuf:"bytes,1,opt,name=Expression"` + // Message represents the message displayed when validation fails. The message is required if the Expression contains + // line breaks. The message must not contain line breaks. + // If unset, the message is "failed rule: {Rule}". + // e.g. "must be a URL with the host matching spec.host" + // If the Expression contains line breaks. Message is required. + // The message must not contain line breaks. + // If unset, the message is "failed Expression: {Expression}". // +optional - MatchPolicy *MatchPolicyType `json:"matchPolicy,omitempty" protobuf:"bytes,9,opt,name=matchPolicy,casttype=MatchPolicyType"` + Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"` + // Reason represents a machine-readable description of why this validation failed. + // If this is the first validation in the list to fail, this reason, as well as the + // corresponding HTTP response code, are used in the + // HTTP response to the client. + // The currently supported reasons are: "Unauthorized", "Forbidden", "Invalid", "RequestEntityTooLarge". + // If not set, StatusReasonInvalid is used in the response to the client. + // +optional + Reason *metav1.StatusReason `json:"reason,omitempty" protobuf:"bytes,3,opt,name=reason"` +} - // NamespaceSelector decides whether to run the webhook on an object based +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.26 + +// ValidatingAdmissionPolicyBinding binds the ValidatingAdmissionPolicy with paramerized resources. +// ValidatingAdmissionPolicyBinding and parameter CRDs together define how cluster administrators configure policies for clusters. +type ValidatingAdmissionPolicyBinding struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the desired behavior of the ValidatingAdmissionPolicyBinding. + Spec ValidatingAdmissionPolicyBindingSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.26 + +// ValidatingAdmissionPolicyBindingList is a list of ValidatingAdmissionPolicyBinding. +type ValidatingAdmissionPolicyBindingList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // List of PolicyBinding. + Items []ValidatingAdmissionPolicyBinding `json:"items,omitempty" protobuf:"bytes,2,rep,name=items"` +} + +// ValidatingAdmissionPolicyBindingSpec is the specification of the ValidatingAdmissionPolicyBinding. +type ValidatingAdmissionPolicyBindingSpec struct { + // PolicyName references a ValidatingAdmissionPolicy name which the ValidatingAdmissionPolicyBinding binds to. + // If the referenced resource does not exist, this binding is considered invalid and will be ignored + // Required. + PolicyName string `json:"policyName,omitempty" protobuf:"bytes,1,rep,name=policyName"` + + // ParamRef specifies the parameter resource used to configure the admission control policy. + // It should point to a resource of the type specified in ParamKind of the bound ValidatingAdmissionPolicy. + // If the policy specifies a ParamKind and the resource referred to by ParamRef does not exist, this binding is considered mis-configured and the FailurePolicy of the ValidatingAdmissionPolicy applied. + // +optional + ParamRef *ParamRef `json:"paramRef,omitempty" protobuf:"bytes,2,rep,name=paramRef"` + + // MatchResources declares what resources match this binding and will be validated by it. + // Note that this is intersected with the policy's matchConstraints, so only requests that are matched by the policy can be selected by this. + // If this is unset, all resources matched by the policy are validated by this binding + // When resourceRules is unset, it does not constrain resource matching. If a resource is matched by the other fields of this object, it will be validated. + // Note that this is differs from ValidatingAdmissionPolicy matchConstraints, where resourceRules are required. + // +optional + MatchResources *MatchResources `json:"matchResources,omitempty" protobuf:"bytes,3,rep,name=matchResources"` +} + +// ParamRef references a parameter resource +// +structType=atomic +type ParamRef struct { + // Name of the resource being referenced. + Name string `json:"name,omitempty" protobuf:"bytes,1,rep,name=name"` + // Namespace of the referenced resource. + // Should be empty for the cluster-scoped resources + // +optional + Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,rep,name=namespace"` +} + +// MatchResources decides whether to run the admission control policy on an object based +// on whether it meets the match criteria. +// The exclude rules take precedence over include rules (if a resource matches both, it is excluded) +// +structType=atomic +type MatchResources struct { + // NamespaceSelector decides whether to run the admission control policy on an object based // on whether the namespace for that object matches the selector. If the // object itself is a namespace, the matching is performed on // object.metadata.labels. If the object is another cluster scoped resource, - // it never skips the webhook. + // it never skips the policy. // // For example, to run the webhook on any objects whose namespace is not // associated with "runlevel" of "0" or "1"; you will set the selector as @@ -242,136 +280,7 @@ type ValidatingWebhook struct { // ] // } // - // If instead you want to only run the webhook on any objects whose - // namespace is associated with the "environment" of "prod" or "staging"; - // you will set the selector as follows: - // "namespaceSelector": { - // "matchExpressions": [ - // { - // "key": "environment", - // "operator": "In", - // "values": [ - // "prod", - // "staging" - // ] - // } - // ] - // } - // - // See - // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels - // for more examples of label selectors. - // - // Default to the empty LabelSelector, which matches everything. - // +optional - NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" protobuf:"bytes,5,opt,name=namespaceSelector"` - - // ObjectSelector decides whether to run the webhook based on if the - // object has matching labels. objectSelector is evaluated against both - // the oldObject and newObject that would be sent to the webhook, and - // is considered to match if either object matches the selector. A null - // object (oldObject in the case of create, or newObject in the case of - // delete) or an object that cannot have labels (like a - // DeploymentRollback or a PodProxyOptions object) is not considered to - // match. - // Use the object selector only if the webhook is opt-in, because end - // users may skip the admission webhook by setting the labels. - // Default to the empty LabelSelector, which matches everything. - // +optional - ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty" protobuf:"bytes,10,opt,name=objectSelector"` - - // SideEffects states whether this webhook has side effects. - // Acceptable values are: None, NoneOnDryRun (webhooks created via v1beta1 may also specify Some or Unknown). - // Webhooks with side effects MUST implement a reconciliation system, since a request may be - // rejected by a future step in the admission chain and the side effects therefore need to be undone. - // Requests with the dryRun attribute will be auto-rejected if they match a webhook with - // sideEffects == Unknown or Some. - SideEffects *SideEffectClass `json:"sideEffects" protobuf:"bytes,6,opt,name=sideEffects,casttype=SideEffectClass"` - - // TimeoutSeconds specifies the timeout for this webhook. After the timeout passes, - // the webhook call will be ignored or the API call will fail based on the - // failure policy. - // The timeout value must be between 1 and 30 seconds. - // Default to 10 seconds. - // +optional - TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty" protobuf:"varint,7,opt,name=timeoutSeconds"` - - // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview` - // versions the Webhook expects. API server will try to use first version in - // the list which it supports. If none of the versions specified in this list - // supported by API server, validation will fail for this object. - // If a persisted webhook configuration specifies allowed versions and does not - // include any versions known to the API Server, calls to the webhook will fail - // and be subject to the failure policy. - AdmissionReviewVersions []string `json:"admissionReviewVersions" protobuf:"bytes,8,rep,name=admissionReviewVersions"` -} - -// MutatingWebhook describes an admission webhook and the resources and operations it applies to. -type MutatingWebhook struct { - // The name of the admission webhook. - // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where - // "imagepolicy" is the name of the webhook, and kubernetes.io is the name - // of the organization. - // Required. - Name string `json:"name" protobuf:"bytes,1,opt,name=name"` - - // ClientConfig defines how to communicate with the hook. - // Required - ClientConfig WebhookClientConfig `json:"clientConfig" protobuf:"bytes,2,opt,name=clientConfig"` - - // Rules describes what operations on what resources/subresources the webhook cares about. - // The webhook cares about an operation if it matches _any_ Rule. - // However, in order to prevent ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks - // from putting the cluster in a state which cannot be recovered from without completely - // disabling the plugin, ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks are never called - // on admission requests for ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects. - Rules []RuleWithOperations `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"` - - // FailurePolicy defines how unrecognized errors from the admission endpoint are handled - - // allowed values are Ignore or Fail. Defaults to Fail. - // +optional - FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"` - - // matchPolicy defines how the "rules" list is used to match incoming requests. - // Allowed values are "Exact" or "Equivalent". - // - // - Exact: match a request only if it exactly matches a specified rule. - // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, - // but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, - // a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the webhook. - // - // - Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. - // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, - // and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, - // a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the webhook. - // - // Defaults to "Equivalent" - // +optional - MatchPolicy *MatchPolicyType `json:"matchPolicy,omitempty" protobuf:"bytes,9,opt,name=matchPolicy,casttype=MatchPolicyType"` - - // NamespaceSelector decides whether to run the webhook on an object based - // on whether the namespace for that object matches the selector. If the - // object itself is a namespace, the matching is performed on - // object.metadata.labels. If the object is another cluster scoped resource, - // it never skips the webhook. - // - // For example, to run the webhook on any objects whose namespace is not - // associated with "runlevel" of "0" or "1"; you will set the selector as - // follows: - // "namespaceSelector": { - // "matchExpressions": [ - // { - // "key": "runlevel", - // "operator": "NotIn", - // "values": [ - // "0", - // "1" - // ] - // } - // ] - // } - // - // If instead you want to only run the webhook on any objects whose + // If instead you want to only run the policy on any objects whose // namespace is associated with the "environment" of "prod" or "staging"; // you will set the selector as follows: // "namespaceSelector": { @@ -393,11 +302,10 @@ type MutatingWebhook struct { // // Default to the empty LabelSelector, which matches everything. // +optional - NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" protobuf:"bytes,5,opt,name=namespaceSelector"` - - // ObjectSelector decides whether to run the webhook based on if the + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" protobuf:"bytes,1,opt,name=namespaceSelector"` + // ObjectSelector decides whether to run the validation based on if the // object has matching labels. objectSelector is evaluated against both - // the oldObject and newObject that would be sent to the webhook, and + // the oldObject and newObject that would be sent to the cel validation, and // is considered to match if either object matches the selector. A null // object (oldObject in the case of create, or newObject in the case of // delete) or an object that cannot have labels (like a @@ -407,155 +315,59 @@ type MutatingWebhook struct { // users may skip the admission webhook by setting the labels. // Default to the empty LabelSelector, which matches everything. // +optional - ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty" protobuf:"bytes,11,opt,name=objectSelector"` - - // SideEffects states whether this webhook has side effects. - // Acceptable values are: None, NoneOnDryRun (webhooks created via v1beta1 may also specify Some or Unknown). - // Webhooks with side effects MUST implement a reconciliation system, since a request may be - // rejected by a future step in the admission chain and the side effects therefore need to be undone. - // Requests with the dryRun attribute will be auto-rejected if they match a webhook with - // sideEffects == Unknown or Some. - SideEffects *SideEffectClass `json:"sideEffects" protobuf:"bytes,6,opt,name=sideEffects,casttype=SideEffectClass"` - - // TimeoutSeconds specifies the timeout for this webhook. After the timeout passes, - // the webhook call will be ignored or the API call will fail based on the - // failure policy. - // The timeout value must be between 1 and 30 seconds. - // Default to 10 seconds. + ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty" protobuf:"bytes,2,opt,name=objectSelector"` + // ResourceRules describes what operations on what resources/subresources the ValidatingAdmissionPolicy matches. + // The policy cares about an operation if it matches _any_ Rule. + // +listType=atomic // +optional - TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty" protobuf:"varint,7,opt,name=timeoutSeconds"` - - // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview` - // versions the Webhook expects. API server will try to use first version in - // the list which it supports. If none of the versions specified in this list - // supported by API server, validation will fail for this object. - // If a persisted webhook configuration specifies allowed versions and does not - // include any versions known to the API Server, calls to the webhook will fail - // and be subject to the failure policy. - AdmissionReviewVersions []string `json:"admissionReviewVersions" protobuf:"bytes,8,rep,name=admissionReviewVersions"` - - // reinvocationPolicy indicates whether this webhook should be called multiple times as part of a single admission evaluation. - // Allowed values are "Never" and "IfNeeded". - // - // Never: the webhook will not be called more than once in a single admission evaluation. - // - // IfNeeded: the webhook will be called at least one additional time as part of the admission evaluation - // if the object being admitted is modified by other admission plugins after the initial webhook call. - // Webhooks that specify this option *must* be idempotent, able to process objects they previously admitted. - // Note: - // * the number of additional invocations is not guaranteed to be exactly one. - // * if additional invocations result in further modifications to the object, webhooks are not guaranteed to be invoked again. - // * webhooks that use this option may be reordered to minimize the number of additional invocations. - // * to validate an object after all mutations are guaranteed complete, use a validating admission webhook instead. - // - // Defaults to "Never". + ResourceRules []NamedRuleWithOperations `json:"resourceRules,omitempty" protobuf:"bytes,3,rep,name=resourceRules"` + // ExcludeResourceRules describes what operations on what resources/subresources the ValidatingAdmissionPolicy should not care about. + // The exclude rules take precedence over include rules (if a resource matches both, it is excluded) + // +listType=atomic // +optional - ReinvocationPolicy *ReinvocationPolicyType `json:"reinvocationPolicy,omitempty" protobuf:"bytes,10,opt,name=reinvocationPolicy,casttype=ReinvocationPolicyType"` + ExcludeResourceRules []NamedRuleWithOperations `json:"excludeResourceRules,omitempty" protobuf:"bytes,4,rep,name=excludeResourceRules"` + // matchPolicy defines how the "MatchResources" list is used to match incoming requests. + // Allowed values are "Exact" or "Equivalent". + // + // - Exact: match a request only if it exactly matches a specified rule. + // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, + // but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, + // a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the ValidatingAdmissionPolicy. + // + // - Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. + // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, + // and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, + // a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the ValidatingAdmissionPolicy. + // + // Defaults to "Equivalent" + // +optional + MatchPolicy *MatchPolicyType `json:"matchPolicy,omitempty" protobuf:"bytes,7,opt,name=matchPolicy,casttype=MatchPolicyType"` } -// ReinvocationPolicyType specifies what type of policy the admission hook uses. -// +enum -type ReinvocationPolicyType string - -const ( - // NeverReinvocationPolicy indicates that the webhook must not be called more than once in a - // single admission evaluation. - NeverReinvocationPolicy ReinvocationPolicyType = "Never" - // IfNeededReinvocationPolicy indicates that the webhook may be called at least one - // additional time as part of the admission evaluation if the object being admitted is - // modified by other admission plugins after the initial webhook call. - IfNeededReinvocationPolicy ReinvocationPolicyType = "IfNeeded" -) +// NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames. +// +structType=atomic +type NamedRuleWithOperations struct { + // ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. + // +listType=atomic + // +optional + ResourceNames []string `json:"resourceNames,omitempty" protobuf:"bytes,1,rep,name=resourceNames"` + // RuleWithOperations is a tuple of Operations and Resources. + RuleWithOperations `json:",inline" protobuf:"bytes,2,opt,name=ruleWithOperations"` +} // RuleWithOperations is a tuple of Operations and Resources. It is recommended to make // sure that all the tuple expansions are valid. -type RuleWithOperations struct { - // Operations is the operations the admission hook cares about - CREATE, UPDATE, DELETE, CONNECT or * - // for all of those operations and any future admission operations that are added. - // If '*' is present, the length of the slice must be one. - // Required. - Operations []OperationType `json:"operations,omitempty" protobuf:"bytes,1,rep,name=operations,casttype=OperationType"` - // Rule is embedded, it describes other criteria of the rule, like - // APIGroups, APIVersions, Resources, etc. - Rule `json:",inline" protobuf:"bytes,2,opt,name=rule"` -} +type RuleWithOperations = v1.RuleWithOperations // OperationType specifies an operation for a request. // +enum -type OperationType string +type OperationType = v1.OperationType // The constants should be kept in sync with those defined in k8s.io/kubernetes/pkg/admission/interface.go. const ( - OperationAll OperationType = "*" - Create OperationType = "CREATE" - Update OperationType = "UPDATE" - Delete OperationType = "DELETE" - Connect OperationType = "CONNECT" + OperationAll OperationType = v1.OperationAll + Create OperationType = v1.Create + Update OperationType = v1.Update + Delete OperationType = v1.Delete + Connect OperationType = v1.Connect ) - -// WebhookClientConfig contains the information to make a TLS -// connection with the webhook -type WebhookClientConfig struct { - // `url` gives the location of the webhook, in standard URL form - // (`scheme://host:port/path`). Exactly one of `url` or `service` - // must be specified. - // - // The `host` should not refer to a service running in the cluster; use - // the `service` field instead. The host might be resolved via external - // DNS in some apiservers (e.g., `kube-apiserver` cannot resolve - // in-cluster DNS as that would be a layering violation). `host` may - // also be an IP address. - // - // Please note that using `localhost` or `127.0.0.1` as a `host` is - // risky unless you take great care to run this webhook on all hosts - // which run an apiserver which might need to make calls to this - // webhook. Such installs are likely to be non-portable, i.e., not easy - // to turn up in a new cluster. - // - // The scheme must be "https"; the URL must begin with "https://". - // - // A path is optional, and if present may be any string permissible in - // a URL. You may use the path to pass an arbitrary string to the - // webhook, for example, a cluster identifier. - // - // Attempting to use a user or basic auth e.g. "user:password@" is not - // allowed. Fragments ("#...") and query parameters ("?...") are not - // allowed, either. - // - // +optional - URL *string `json:"url,omitempty" protobuf:"bytes,3,opt,name=url"` - - // `service` is a reference to the service for this webhook. Either - // `service` or `url` must be specified. - // - // If the webhook is running within the cluster, then you should use `service`. - // - // +optional - Service *ServiceReference `json:"service,omitempty" protobuf:"bytes,1,opt,name=service"` - - // `caBundle` is a PEM encoded CA bundle which will be used to validate the webhook's server certificate. - // If unspecified, system trust roots on the apiserver are used. - // +optional - CABundle []byte `json:"caBundle,omitempty" protobuf:"bytes,2,opt,name=caBundle"` -} - -// ServiceReference holds a reference to Service.legacy.k8s.io -type ServiceReference struct { - // `namespace` is the namespace of the service. - // Required - Namespace string `json:"namespace" protobuf:"bytes,1,opt,name=namespace"` - // `name` is the name of the service. - // Required - Name string `json:"name" protobuf:"bytes,2,opt,name=name"` - - // `path` is an optional URL path which will be sent in any request to - // this service. - // +optional - Path *string `json:"path,omitempty" protobuf:"bytes,3,opt,name=path"` - - // If specified, the port on the service that hosting webhook. - // Default to 443 for backward compatibility. - // `port` should be a valid port number (1-65535, inclusive). - // +optional - Port *int32 `json:"port,omitempty" protobuf:"varint,4,opt,name=port"` -} diff --git a/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go b/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go index 630ea1f57b4..5fdf8e3fa78 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go +++ b/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go @@ -17,63 +17,25 @@ limitations under the License. package v1beta1 import ( + v1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Rule is a tuple of APIGroups, APIVersion, and Resources.It is recommended // to make sure that all the tuple expansions are valid. -type Rule struct { - // APIGroups is the API groups the resources belong to. '*' is all groups. - // If '*' is present, the length of the slice must be one. - // Required. - APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,1,rep,name=apiGroups"` - - // APIVersions is the API versions the resources belong to. '*' is all versions. - // If '*' is present, the length of the slice must be one. - // Required. - APIVersions []string `json:"apiVersions,omitempty" protobuf:"bytes,2,rep,name=apiVersions"` - - // Resources is a list of resources this rule applies to. - // - // For example: - // 'pods' means pods. - // 'pods/log' means the log subresource of pods. - // '*' means all resources, but not subresources. - // 'pods/*' means all subresources of pods. - // '*/scale' means all scale subresources. - // '*/*' means all resources and their subresources. - // - // If wildcard is present, the validation rule will ensure resources do not - // overlap with each other. - // - // Depending on the enclosing object, subresources might not be allowed. - // Required. - Resources []string `json:"resources,omitempty" protobuf:"bytes,3,rep,name=resources"` - - // scope specifies the scope of this rule. - // Valid values are "Cluster", "Namespaced", and "*" - // "Cluster" means that only cluster-scoped resources will match this rule. - // Namespace API objects are cluster-scoped. - // "Namespaced" means that only namespaced resources will match this rule. - // "*" means that there are no scope restrictions. - // Subresources match the scope of their parent resource. - // Default is "*". - // - // +optional - Scope *ScopeType `json:"scope,omitempty" protobuf:"bytes,4,rep,name=scope"` -} +type Rule = v1.Rule // ScopeType specifies a scope for a Rule. -type ScopeType string +type ScopeType = v1.ScopeType const ( // ClusterScope means that scope is limited to cluster-scoped objects. // Namespace objects are cluster-scoped. - ClusterScope ScopeType = "Cluster" + ClusterScope ScopeType = v1.ClusterScope // NamespacedScope means that scope is limited to namespaced objects. - NamespacedScope ScopeType = "Namespaced" + NamespacedScope ScopeType = v1.NamespacedScope // AllScopes means that all scopes are included. - AllScopes ScopeType = "*" + AllScopes ScopeType = v1.AllScopes ) // FailurePolicyType specifies a failure policy that defines how unrecognized errors from the admission endpoint are handled. @@ -488,27 +450,19 @@ const ( // RuleWithOperations is a tuple of Operations and Resources. It is recommended to make // sure that all the tuple expansions are valid. -type RuleWithOperations struct { - // Operations is the operations the admission hook cares about - CREATE, UPDATE, DELETE, CONNECT or * - // for all of those operations and any future admission operations that are added. - // If '*' is present, the length of the slice must be one. - // Required. - Operations []OperationType `json:"operations,omitempty" protobuf:"bytes,1,rep,name=operations,casttype=OperationType"` - // Rule is embedded, it describes other criteria of the rule, like - // APIGroups, APIVersions, Resources, etc. - Rule `json:",inline" protobuf:"bytes,2,opt,name=rule"` -} +type RuleWithOperations = v1.RuleWithOperations // OperationType specifies an operation for a request. -type OperationType string +// +enum +type OperationType = v1.OperationType // The constants should be kept in sync with those defined in k8s.io/kubernetes/pkg/admission/interface.go. const ( - OperationAll OperationType = "*" - Create OperationType = "CREATE" - Update OperationType = "UPDATE" - Delete OperationType = "DELETE" - Connect OperationType = "CONNECT" + OperationAll OperationType = v1.OperationAll + Create OperationType = v1.Create + Update OperationType = v1.Update + Delete OperationType = v1.Delete + Connect OperationType = v1.Connect ) // WebhookClientConfig contains the information to make a TLS diff --git a/staging/src/k8s.io/api/roundtrip_test.go b/staging/src/k8s.io/api/roundtrip_test.go index 665299163cc..004b2fc341b 100644 --- a/staging/src/k8s.io/api/roundtrip_test.go +++ b/staging/src/k8s.io/api/roundtrip_test.go @@ -23,6 +23,7 @@ import ( admissionv1 "k8s.io/api/admission/v1" admissionv1beta1 "k8s.io/api/admission/v1beta1" admissionregv1 "k8s.io/api/admissionregistration/v1" + admissionregv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" admissionregv1beta1 "k8s.io/api/admissionregistration/v1beta1" apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" apiserverinternalv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1" @@ -83,6 +84,7 @@ import ( var groups = []runtime.SchemeBuilder{ admissionv1beta1.SchemeBuilder, admissionv1.SchemeBuilder, + admissionregv1alpha1.SchemeBuilder, admissionregv1beta1.SchemeBuilder, admissionregv1.SchemeBuilder, apiserverinternalv1alpha1.SchemeBuilder, diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go index c706b03c5fe..d7628a515b3 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go @@ -75,6 +75,8 @@ var ( initEnvErr error ) +// This func is duplicated in k8s.io/apiserver/pkg/admission/plugin/cel/internal/implementation.go +// If any changes are made here, consider to make the same changes there as well. func getBaseEnv() (*cel.Env, error) { initEnvOnce.Do(func() { var opts []cel.EnvOption diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compiler.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compiler.go new file mode 100644 index 00000000000..dac2f2ab05c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compiler.go @@ -0,0 +1,220 @@ +/* +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 cel + +import ( + "sync" + + "github.com/google/cel-go/cel" + + apiservercel "k8s.io/apiserver/pkg/cel" + "k8s.io/apiserver/pkg/cel/library" +) + +const ( + ObjectVarName = "object" + OldObjectVarName = "oldObject" + ParamsVarName = "params" + RequestVarName = "request" + + checkFrequency = 100 +) + +type envs struct { + noParams *cel.Env + withParams *cel.Env +} + +var ( + initEnvsOnce sync.Once + initEnvs *envs + initEnvsErr error +) + +func getEnvs() (*envs, error) { + initEnvsOnce.Do(func() { + base, err := buildBaseEnv() + if err != nil { + initEnvsErr = err + return + } + noParams, err := buildNoParamsEnv(base) + if err != nil { + initEnvsErr = err + return + } + withParams, err := buildWithParamsEnv(noParams) + if err != nil { + initEnvsErr = err + return + } + initEnvs = &envs{noParams: noParams, withParams: withParams} + }) + return initEnvs, initEnvsErr +} + +// This is a similar code as in k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go +// If any changes are made here, consider to make the same changes there as well. +func buildBaseEnv() (*cel.Env, error) { + var opts []cel.EnvOption + opts = append(opts, cel.HomogeneousAggregateLiterals()) + // Validate function declarations once during base env initialization, + // so they don't need to be evaluated each time a CEL rule is compiled. + // This is a relatively expensive operation. + opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true)) + opts = append(opts, library.ExtensionLibs...) + + return cel.NewEnv(opts...) +} + +func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) { + var propDecls []cel.EnvOption + reg := apiservercel.NewRegistry(baseEnv) + + requestType := buildRequestType() + rt, err := apiservercel.NewRuleTypes(requestType.TypeName(), requestType, reg) + if err != nil { + return nil, err + } + if rt == nil { + return nil, nil + } + opts, err := rt.EnvOptions(baseEnv.TypeProvider()) + if err != nil { + return nil, err + } + propDecls = append(propDecls, cel.Variable(ObjectVarName, cel.DynType)) + propDecls = append(propDecls, cel.Variable(OldObjectVarName, cel.DynType)) + propDecls = append(propDecls, cel.Variable(RequestVarName, requestType.CelType())) + + opts = append(opts, propDecls...) + env, err := baseEnv.Extend(opts...) + if err != nil { + return nil, err + } + return env, nil +} + +func buildWithParamsEnv(noParams *cel.Env) (*cel.Env, error) { + return noParams.Extend(cel.Variable(ParamsVarName, cel.DynType)) +} + +// buildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that +// converts the native type definition to apiservercel.DeclType once such a utility becomes available. +// The 'uid' field is omitted since it is not needed for in-process admission review. +// The 'object' and 'oldObject' fields are omitted since they are exposed as root level CEL variables. +func buildRequestType() *apiservercel.DeclType { + field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField { + return apiservercel.NewDeclField(name, declType, required, nil, nil) + } + fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField { + result := make(map[string]*apiservercel.DeclField, len(fields)) + for _, f := range fields { + result[f.Name] = f + } + return result + } + gvkType := apiservercel.NewObjectType("kubernetes.GroupVersionKind", fields( + field("group", apiservercel.StringType, true), + field("version", apiservercel.StringType, true), + field("kind", apiservercel.StringType, true), + )) + gvrType := apiservercel.NewObjectType("kubernetes.GroupVersionResource", fields( + field("group", apiservercel.StringType, true), + field("version", apiservercel.StringType, true), + field("resource", apiservercel.StringType, true), + )) + userInfoType := apiservercel.NewObjectType("kubernetes.UserInfo", fields( + field("username", apiservercel.StringType, false), + field("uid", apiservercel.StringType, false), + field("groups", apiservercel.NewListType(apiservercel.StringType, -1), false), + field("extra", apiservercel.NewMapType(apiservercel.StringType, apiservercel.NewListType(apiservercel.StringType, -1), -1), false), + )) + return apiservercel.NewObjectType("kubernetes.AdmissionRequest", fields( + field("kind", gvkType, true), + field("resource", gvrType, true), + field("subResource", apiservercel.StringType, false), + field("requestKind", gvkType, true), + field("requestResource", gvrType, true), + field("requestSubResource", apiservercel.StringType, false), + field("name", apiservercel.StringType, true), + field("namespace", apiservercel.StringType, false), + field("operation", apiservercel.StringType, true), + field("userInfo", userInfoType, true), + field("dryRun", apiservercel.BoolType, false), + field("options", apiservercel.DynType, false), + )) +} + +// CompilationResult represents a compiled ValidatingAdmissionPolicy validation expression. +type CompilationResult struct { + Program cel.Program + Error *apiservercel.Error +} + +// CompileValidatingPolicyExpression returns a compiled vaalidating policy CEL expression. +func CompileValidatingPolicyExpression(validationExpression string, hasParams bool) CompilationResult { + var env *cel.Env + envs, err := getEnvs() + if err != nil { + return CompilationResult{ + Error: &apiservercel.Error{ + Type: apiservercel.ErrorTypeInternal, + Detail: "compiler initialization failed: " + err.Error(), + }, + } + } + if hasParams { + env = envs.withParams + } else { + env = envs.noParams + } + + ast, issues := env.Compile(validationExpression) + if issues != nil { + return CompilationResult{ + Error: &apiservercel.Error{ + Type: apiservercel.ErrorTypeInvalid, + Detail: "compilation failed: " + issues.String(), + }, + } + } + if ast.OutputType() != cel.BoolType { + return CompilationResult{ + Error: &apiservercel.Error{ + Type: apiservercel.ErrorTypeInvalid, + Detail: "cel expression must evaluate to a bool", + }, + } + } + prog, err := env.Program(ast, + cel.EvalOptions(cel.OptOptimize), + cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...), + cel.InterruptCheckFrequency(checkFrequency), + ) + if err != nil { + return CompilationResult{ + Error: &apiservercel.Error{ + Type: apiservercel.ErrorTypeInvalid, + Detail: "program instantiation failed: " + err.Error(), + }, + } + } + return CompilationResult{ + Program: prog, + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compiler_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compiler_test.go new file mode 100644 index 00000000000..3bae35a22ed --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compiler_test.go @@ -0,0 +1,125 @@ +/* +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 cel + +import ( + "strings" + "testing" +) + +func TestCompileValidatingPolicyExpression(t *testing.T) { + cases := []struct { + name string + expressions []string + hasParams bool + errorExpressions map[string]string + }{ + { + name: "invalid syntax", + errorExpressions: map[string]string{ + "1 < 'asdf'": "found no matching overload for '_<_' applied to '(int, string)", + "'asdf'.contains('x'": "Syntax error: missing ')' at", + }, + }, + { + name: "with params", + expressions: []string{"object.foo < params.x"}, + hasParams: true, + }, + { + name: "without params", + errorExpressions: map[string]string{"object.foo < params.x": "undeclared reference to 'params'"}, + hasParams: false, + }, + { + name: "oldObject comparison", + expressions: []string{"object.foo == oldObject.foo"}, + }, + { + name: "object null checks", + // since object and oldObject are CEL variable, has() cannot be used (it works only on fields), + // so we always populate it to allow for a null check in the case of CREATE, where oldObject is + // null, and DELETE, where object is null. + expressions: []string{"object == null || oldObject == null || object.foo == oldObject.foo"}, + }, + { + name: "invalid root var", + errorExpressions: map[string]string{"object.foo < invalid.x": "undeclared reference to 'invalid'"}, + hasParams: false, + }, + { + name: "function library", + // sanity check that functions of the various libraries are available + expressions: []string{ + "object.spec.string.matches('[0-9]+')", // strings extension lib + "object.spec.string.findAll('[0-9]+').size() > 0", // kubernetes string lib + "object.spec.list.isSorted()", // kubernetes list lib + "url(object.spec.endpoint).getHostname() in ['ok1', 'ok2']", // kubernetes url lib + }, + }, + { + name: "valid request", + expressions: []string{ + "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'", + "request.resource.group == 'example.com' && request.resource.version == 'v1' && request.resource.resource == 'fake' && request.subResource == 'scale'", + "request.requestKind.group == 'example.com' && request.requestKind.version == 'v1' && request.requestKind.kind == 'Fake'", + "request.requestResource.group == 'example.com' && request.requestResource.version == 'v1' && request.requestResource.resource == 'fake' && request.requestSubResource == 'scale'", + "request.name == 'fake-name'", + "request.namespace == 'fake-namespace'", + "request.operation == 'CREATE'", + "request.userInfo.username == 'admin'", + "request.userInfo.uid == '014fbff9a07c'", + "request.userInfo.groups == ['system:authenticated', 'my-admin-group']", + "request.userInfo.extra == {'some-key': ['some-value1', 'some-value2']}", + "request.dryRun == false", + "request.options == {'whatever': 'you want'}", + }, + }, + { + name: "invalid request", + errorExpressions: map[string]string{ + "request.foo1 == 'nope'": "undefined field 'foo1'", + "request.resource.foo2 == 'nope'": "undefined field 'foo2'", + "request.requestKind.foo3 == 'nope'": "undefined field 'foo3'", + "request.requestResource.foo4 == 'nope'": "undefined field 'foo4'", + "request.userInfo.foo5 == 'nope'": "undefined field 'foo5'", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + for _, expr := range tc.expressions { + result := CompileValidatingPolicyExpression(expr, tc.hasParams) + if result.Error != nil { + t.Errorf("Unexpected error: %v", result.Error) + } + } + for expr, expectErr := range tc.errorExpressions { + result := CompileValidatingPolicyExpression(expr, tc.hasParams) + if result.Error == nil { + t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr) + continue + } + if !strings.Contains(result.Error.Error(), expectErr) { + t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error) + } + continue + } + }) + } +} diff --git a/test/integration/etcd/data.go b/test/integration/etcd/data.go index 01d37339899..8177c2479e7 100644 --- a/test/integration/etcd/data.go +++ b/test/integration/etcd/data.go @@ -386,6 +386,17 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes }, // -- + // k8s.io/kubernetes/pkg/apis/admissionregistration/v1alpha1 + gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): { + Stub: `{"metadata":{"name":"vap1","creationTimestamp":null},"spec":{"paramKind":{"apiVersion":"test.example.com/v1","kind":"Example"},"matchConstraints":{"resourceRules": [{"resourceNames": ["fakeName"], "apiGroups":["apps"],"apiVersions":["v1"],"operations":["CREATE", "UPDATE"], "resources":["deployments"]}]},"validations":[{"expression":"object.spec.replicas <= params.maxReplicas","message":"Too many replicas"}]}}`, + ExpectedEtcdPath: "/registry/validatingadmissionpolicies/vap1", + }, + gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"): { + Stub: `{"metadata":{"name":"pb1","creationTimestamp":null},"spec":{"policyName":"replicalimit-policy.example.com","paramRef":{"name":"replica-limit-test.example.com"}}}`, + ExpectedEtcdPath: "/registry/validatingadmissionpolicybindings/pb1", + }, + // -- + // k8s.io/kubernetes/pkg/apis/scheduling/v1 gvr("scheduling.k8s.io", "v1", "priorityclasses"): { Stub: `{"metadata":{"name":"pc3"},"Value":1000}`,