ValidatingAdmissionPolicy controller for Type Checking (#117377)

* [API REVIEW] ValidatingAdmissionPolicyStatucController config.

worker count.

* ValidatingAdmissionPolicyStatus controller.

* remove CEL typechecking from API server.

* fix initializer tests.

* remove type checking integration tests

from API server integration tests.

* validatingadmissionpolicy-status options.

* grant access to VAP controller.

* add defaulting unit test.

* generated: ./hack/update-codegen.sh

* add OWNERS for VAP status controller.

* type checking test case.
This commit is contained in:
Jiahui Feng
2023-07-13 13:41:50 -07:00
committed by GitHub
parent a9e40bd7c6
commit 049614f884
42 changed files with 2242 additions and 1392 deletions

View File

@@ -531,6 +531,7 @@ API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,K
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,ServiceController
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,StatefulSetController
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,TTLAfterFinishedController
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,ValidatingAdmissionPolicyStatusController
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,LegacySATokenCleanerConfiguration,CleanUpPeriod
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,NamespaceControllerConfiguration,ConcurrentNamespaceSyncs
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,NamespaceControllerConfiguration,NamespaceSyncPeriod
@@ -567,6 +568,7 @@ API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,S
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,SAControllerConfiguration,ServiceAccountKeyFile
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,StatefulSetControllerConfiguration,ConcurrentStatefulSetSyncs
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,TTLAfterFinishedControllerConfiguration,ConcurrentTTLSyncs
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,ValidatingAdmissionPolicyStatusControllerConfiguration,ConcurrentPolicySyncs
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,EnableDynamicProvisioning
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,EnableHostPathProvisioning
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,FlexVolumePluginDir

View File

@@ -35,7 +35,6 @@ import (
utilnet "k8s.io/apimachinery/pkg/util/net"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/egressselector"
@@ -45,7 +44,6 @@ import (
"k8s.io/client-go/dynamic"
clientgoinformers "k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
k8sscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/util/keyutil"
cliflag "k8s.io/component-base/cli/flag"
@@ -285,8 +283,7 @@ func CreateKubeAPIServerConfig(opts options.CompletedOptions) (
CloudConfigFile: opts.CloudProvider.CloudConfigFile,
}
serviceResolver := buildServiceResolver(opts.EnableAggregatorRouting, genericConfig.LoopbackClientConfig.Host, versionedInformers)
schemaResolver := resolver.NewDefinitionsSchemaResolver(k8sscheme.Scheme, genericConfig.OpenAPIConfig.GetDefinitions)
pluginInitializers, admissionPostStartHook, err := admissionConfig.New(proxyTransport, genericConfig.EgressSelector, serviceResolver, genericConfig.TracerProvider, schemaResolver)
pluginInitializers, admissionPostStartHook, err := admissionConfig.New(proxyTransport, genericConfig.EgressSelector, serviceResolver, genericConfig.TracerProvider)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to create admission plugin initializer: %v", err)
}

View File

@@ -484,6 +484,9 @@ func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.LegacyServiceAccountTokenCleanUp) {
register(names.LegacyServiceAccountTokenCleanerController, startLegacySATokenCleaner)
}
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.ValidatingAdmissionPolicy) {
register("validatingadmissionpolicy-status-controller", startValidatingAdmissionPolicyStatusController)
}
return controllers
}

View File

@@ -62,31 +62,32 @@ type KubeControllerManagerOptions struct {
KubeCloudShared *cpoptions.KubeCloudSharedOptions
ServiceController *cpoptions.ServiceControllerOptions
AttachDetachController *AttachDetachControllerOptions
CSRSigningController *CSRSigningControllerOptions
DaemonSetController *DaemonSetControllerOptions
DeploymentController *DeploymentControllerOptions
StatefulSetController *StatefulSetControllerOptions
DeprecatedFlags *DeprecatedControllerOptions
EndpointController *EndpointControllerOptions
EndpointSliceController *EndpointSliceControllerOptions
EndpointSliceMirroringController *EndpointSliceMirroringControllerOptions
EphemeralVolumeController *EphemeralVolumeControllerOptions
GarbageCollectorController *GarbageCollectorControllerOptions
HPAController *HPAControllerOptions
JobController *JobControllerOptions
CronJobController *CronJobControllerOptions
LegacySATokenCleaner *LegacySATokenCleanerOptions
NamespaceController *NamespaceControllerOptions
NodeIPAMController *NodeIPAMControllerOptions
NodeLifecycleController *NodeLifecycleControllerOptions
PersistentVolumeBinderController *PersistentVolumeBinderControllerOptions
PodGCController *PodGCControllerOptions
ReplicaSetController *ReplicaSetControllerOptions
ReplicationController *ReplicationControllerOptions
ResourceQuotaController *ResourceQuotaControllerOptions
SAController *SAControllerOptions
TTLAfterFinishedController *TTLAfterFinishedControllerOptions
AttachDetachController *AttachDetachControllerOptions
CSRSigningController *CSRSigningControllerOptions
DaemonSetController *DaemonSetControllerOptions
DeploymentController *DeploymentControllerOptions
StatefulSetController *StatefulSetControllerOptions
DeprecatedFlags *DeprecatedControllerOptions
EndpointController *EndpointControllerOptions
EndpointSliceController *EndpointSliceControllerOptions
EndpointSliceMirroringController *EndpointSliceMirroringControllerOptions
EphemeralVolumeController *EphemeralVolumeControllerOptions
GarbageCollectorController *GarbageCollectorControllerOptions
HPAController *HPAControllerOptions
JobController *JobControllerOptions
CronJobController *CronJobControllerOptions
LegacySATokenCleaner *LegacySATokenCleanerOptions
NamespaceController *NamespaceControllerOptions
NodeIPAMController *NodeIPAMControllerOptions
NodeLifecycleController *NodeLifecycleControllerOptions
PersistentVolumeBinderController *PersistentVolumeBinderControllerOptions
PodGCController *PodGCControllerOptions
ReplicaSetController *ReplicaSetControllerOptions
ReplicationController *ReplicationControllerOptions
ResourceQuotaController *ResourceQuotaControllerOptions
SAController *SAControllerOptions
TTLAfterFinishedController *TTLAfterFinishedControllerOptions
ValidatingAdmissionPolicyStatusController *ValidatingAdmissionPolicyStatusControllerOptions
SecureServing *apiserveroptions.SecureServingOptionsWithLoopback
Authentication *apiserveroptions.DelegatingAuthenticationOptions
@@ -186,6 +187,9 @@ func NewKubeControllerManagerOptions() (*KubeControllerManagerOptions, error) {
TTLAfterFinishedController: &TTLAfterFinishedControllerOptions{
&componentConfig.TTLAfterFinishedController,
},
ValidatingAdmissionPolicyStatusController: &ValidatingAdmissionPolicyStatusControllerOptions{
&componentConfig.ValidatingAdmissionPolicyStatusController,
},
SecureServing: apiserveroptions.NewSecureServingOptions().WithLoopback(),
Authentication: apiserveroptions.NewDelegatingAuthenticationOptions(),
Authorization: apiserveroptions.NewDelegatingAuthorizationOptions(),
@@ -261,6 +265,7 @@ func (s *KubeControllerManagerOptions) Flags(allControllers []string, disabledBy
s.ResourceQuotaController.AddFlags(fss.FlagSet(names.ResourceQuotaController))
s.SAController.AddFlags(fss.FlagSet(names.ServiceAccountController))
s.TTLAfterFinishedController.AddFlags(fss.FlagSet(names.TTLAfterFinishedController))
s.ValidatingAdmissionPolicyStatusController.AddFlags(fss.FlagSet(names.ValidatingAdmissionPolicyStatusController))
s.Metrics.AddFlags(fss.FlagSet("metrics"))
logsapi.AddFlags(s.Logs, fss.FlagSet("logs"))
@@ -359,6 +364,9 @@ func (s *KubeControllerManagerOptions) ApplyTo(c *kubecontrollerconfig.Config, a
if err := s.TTLAfterFinishedController.ApplyTo(&c.ComponentConfig.TTLAfterFinishedController); err != nil {
return err
}
if err := s.ValidatingAdmissionPolicyStatusController.ApplyTo(&c.ComponentConfig.ValidatingAdmissionPolicyStatusController); err != nil {
return err
}
if err := s.SecureServing.ApplyTo(&c.SecureServing, &c.LoopbackClientConfig); err != nil {
return err
}

View File

@@ -63,6 +63,7 @@ import (
serviceaccountconfig "k8s.io/kubernetes/pkg/controller/serviceaccount/config"
statefulsetconfig "k8s.io/kubernetes/pkg/controller/statefulset/config"
ttlafterfinishedconfig "k8s.io/kubernetes/pkg/controller/ttlafterfinished/config"
validatingadmissionpolicystatusconfig "k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config"
attachdetachconfig "k8s.io/kubernetes/pkg/controller/volume/attachdetach/config"
ephemeralvolumeconfig "k8s.io/kubernetes/pkg/controller/volume/ephemeral/config"
persistentvolumeconfig "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/config"
@@ -101,6 +102,7 @@ var args = []string{
"--concurrent-service-syncs=2",
"--concurrent-serviceaccount-token-syncs=10",
"--concurrent_rc_syncs=10",
"--concurrent-validating-admission-policy-status-syncs=9",
"--configure-cloud-routes=false",
"--contention-profiling=true",
"--controller-start-interval=2m",
@@ -409,6 +411,11 @@ func TestAddFlags(t *testing.T) {
ConcurrentTTLSyncs: 8,
},
},
ValidatingAdmissionPolicyStatusController: &ValidatingAdmissionPolicyStatusControllerOptions{
&validatingadmissionpolicystatusconfig.ValidatingAdmissionPolicyStatusControllerConfiguration{
ConcurrentPolicySyncs: 9,
},
},
SecureServing: (&apiserveroptions.SecureServingOptions{
BindPort: 10001,
BindAddress: netutils.ParseIPSloppy("192.168.4.21"),
@@ -640,6 +647,9 @@ func TestApplyTo(t *testing.T) {
TTLAfterFinishedController: ttlafterfinishedconfig.TTLAfterFinishedControllerConfiguration{
ConcurrentTTLSyncs: 8,
},
ValidatingAdmissionPolicyStatusController: validatingadmissionpolicystatusconfig.ValidatingAdmissionPolicyStatusControllerConfiguration{
ConcurrentPolicySyncs: 9,
},
},
}

View File

@@ -0,0 +1,63 @@
/*
Copyright 2023 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 options
import (
"fmt"
"github.com/spf13/pflag"
validatingadmissionpolicystatusconfig "k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config"
)
// ValidatingAdmissionPolicyStatusControllerOptions holds the ValidatingAdmissionPolicyStatusController options.
type ValidatingAdmissionPolicyStatusControllerOptions struct {
*validatingadmissionpolicystatusconfig.ValidatingAdmissionPolicyStatusControllerConfiguration
}
// AddFlags adds flags related to ValidatingAdmissionPolicyStatusController for controller manager to the specified FlagSet.
func (o *ValidatingAdmissionPolicyStatusControllerOptions) AddFlags(fs *pflag.FlagSet) {
if o == nil {
return
}
fs.Int32Var(&o.ConcurrentPolicySyncs, "concurrent-validating-admission-policy-status-syncs", o.ConcurrentPolicySyncs, "The number of ValidatingAdmissionPolicyStatusController workers that are allowed to sync concurrently.")
}
// ApplyTo fills up ValidatingAdmissionPolicyStatusController config with options.
func (o *ValidatingAdmissionPolicyStatusControllerOptions) ApplyTo(cfg *validatingadmissionpolicystatusconfig.ValidatingAdmissionPolicyStatusControllerConfiguration) error {
if o == nil {
return nil
}
cfg.ConcurrentPolicySyncs = o.ConcurrentPolicySyncs
return nil
}
// Validate checks validation of ValidatingAdmissionPolicyStatusControllerOptions.
func (o *ValidatingAdmissionPolicyStatusControllerOptions) Validate() []error {
if o == nil {
return nil
}
var errs []error
if o.ConcurrentPolicySyncs <= 0 {
// omits controller or flag names because the CLI already includes these in the message.
errs = append(errs, fmt.Errorf("must be positive, got %d", o.ConcurrentPolicySyncs))
}
return errs
}

View File

@@ -0,0 +1,51 @@
/*
Copyright 2023 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 app
import (
"context"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
pluginvalidatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/controller-manager/controller"
"k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus"
"k8s.io/kubernetes/pkg/generated/openapi"
)
var validatingAdmissionPolicyResource = admissionregistrationv1alpha1.SchemeGroupVersion.WithResource("validatingadmissionpolicies")
func startValidatingAdmissionPolicyStatusController(ctx context.Context, controllerContext ControllerContext) (controller.Interface, bool, error) {
// intended check against served resource but not feature gate.
// KCM won't start the controller without the feature gate set.
if !controllerContext.AvailableResources[validatingAdmissionPolicyResource] {
return nil, false, nil
}
typeChecker := &pluginvalidatingadmissionpolicy.TypeChecker{
SchemaResolver: resolver.NewDefinitionsSchemaResolver(scheme.Scheme, openapi.GetOpenAPIDefinitions),
RestMapper: controllerContext.RESTMapper,
}
c, err := validatingadmissionpolicystatus.NewController(
controllerContext.InformerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies(),
controllerContext.ClientBuilder.ClientOrDie("validatingadmissionpolicy-status-controller").AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies(),
typeChecker,
)
go c.Run(ctx, int(controllerContext.ComponentConfig.ValidatingAdmissionPolicyStatusController.ConcurrentPolicySyncs))
return nil, true, err
}

View File

@@ -86,6 +86,7 @@ const (
StorageVersionGarbageCollectorController = "storageversion-garbage-collector-controller"
ResourceClaimController = "resourceclaim-controller"
LegacyServiceAccountTokenCleanerController = "legacy-serviceaccount-token-cleaner-controller"
ValidatingAdmissionPolicyStatusController = "validatingadmissionpolicy-status-controller"
)
// KCMControllerAliases returns a mapping of aliases to canonical controller names

View File

@@ -41,6 +41,7 @@ import (
serviceaccountconfig "k8s.io/kubernetes/pkg/controller/serviceaccount/config"
statefulsetconfig "k8s.io/kubernetes/pkg/controller/statefulset/config"
ttlafterfinishedconfig "k8s.io/kubernetes/pkg/controller/ttlafterfinished/config"
validatingadmissionpolicystatusconfig "k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config"
attachdetachconfig "k8s.io/kubernetes/pkg/controller/volume/attachdetach/config"
ephemeralvolumeconfig "k8s.io/kubernetes/pkg/controller/volume/ephemeral/config"
persistentvolumeconfig "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/config"
@@ -132,6 +133,9 @@ type KubeControllerManagerConfiguration struct {
// TTLAfterFinishedControllerConfiguration holds configuration for
// TTLAfterFinishedController related features.
TTLAfterFinishedController ttlafterfinishedconfig.TTLAfterFinishedControllerConfiguration
// ValidatingAdmissionPolicyStatusControllerConfiguration holds configuration for
// ValidatingAdmissionPolicyStatusController related features.
ValidatingAdmissionPolicyStatusController validatingadmissionpolicystatusconfig.ValidatingAdmissionPolicyStatusControllerConfiguration
}
// DeprecatedControllerConfiguration contains elements be deprecated.

View File

@@ -41,6 +41,7 @@ import (
serviceaccountconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/serviceaccount/config/v1alpha1"
statefulsetconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/statefulset/config/v1alpha1"
ttlafterfinishedconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/ttlafterfinished/config/v1alpha1"
validatingadmissionpolicystatusv1alpha1 "k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config/v1alpha1"
attachdetachconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/volume/attachdetach/config/v1alpha1"
ephemeralvolumeconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/volume/ephemeral/config/v1alpha1"
persistentvolumeconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/config/v1alpha1"
@@ -112,4 +113,6 @@ func SetDefaults_KubeControllerManagerConfiguration(obj *kubectrlmgrconfigv1alph
ttlafterfinishedconfigv1alpha1.RecommendedDefaultTTLAfterFinishedControllerConfiguration(&obj.TTLAfterFinishedController)
// Use the default RecommendedDefaultPersistentVolumeBinderControllerConfiguration options
persistentvolumeconfigv1alpha1.RecommendedDefaultPersistentVolumeBinderControllerConfiguration(&obj.PersistentVolumeBinderController)
// Use the default RecommendedDefaultValidatingAdmissionPolicyStatusControllerConfiguration options
validatingadmissionpolicystatusv1alpha1.RecommendedDefaultValidatingAdmissionPolicyStatusControllerConfiguration(&obj.ValidatingAdmissionPolicyStatusController)
}

View File

@@ -50,6 +50,7 @@ import (
serviceaccountconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/serviceaccount/config/v1alpha1"
statefulsetconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/statefulset/config/v1alpha1"
ttlafterfinishedconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/ttlafterfinished/config/v1alpha1"
validatingadmissionpolicystatusconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config/v1alpha1"
attachdetachconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/volume/attachdetach/config/v1alpha1"
ephemeralconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/volume/ephemeral/config/v1alpha1"
persistentvolumeconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/config/v1alpha1"
@@ -220,6 +221,9 @@ func autoConvert_v1alpha1_KubeControllerManagerConfiguration_To_config_KubeContr
if err := ttlafterfinishedconfigv1alpha1.Convert_v1alpha1_TTLAfterFinishedControllerConfiguration_To_config_TTLAfterFinishedControllerConfiguration(&in.TTLAfterFinishedController, &out.TTLAfterFinishedController, s); err != nil {
return err
}
if err := validatingadmissionpolicystatusconfigv1alpha1.Convert_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration_To_config_ValidatingAdmissionPolicyStatusControllerConfiguration(&in.ValidatingAdmissionPolicyStatusController, &out.ValidatingAdmissionPolicyStatusController, s); err != nil {
return err
}
return nil
}
@@ -313,6 +317,9 @@ func autoConvert_config_KubeControllerManagerConfiguration_To_v1alpha1_KubeContr
if err := ttlafterfinishedconfigv1alpha1.Convert_config_TTLAfterFinishedControllerConfiguration_To_v1alpha1_TTLAfterFinishedControllerConfiguration(&in.TTLAfterFinishedController, &out.TTLAfterFinishedController, s); err != nil {
return err
}
if err := validatingadmissionpolicystatusconfigv1alpha1.Convert_config_ValidatingAdmissionPolicyStatusControllerConfiguration_To_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration(&in.ValidatingAdmissionPolicyStatusController, &out.ValidatingAdmissionPolicyStatusController, s); err != nil {
return err
}
return nil
}

View File

@@ -73,6 +73,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa
out.SAController = in.SAController
out.ServiceController = in.ServiceController
out.TTLAfterFinishedController = in.TTLAfterFinishedController
out.ValidatingAdmissionPolicyStatusController = in.ValidatingAdmissionPolicyStatusController
return
}

View File

@@ -0,0 +1,6 @@
# See the OWNERS docs at https://go.k8s.io/owners
reviewers:
- jiahuif
labels:
- sig/api-machinery

View File

@@ -0,0 +1,19 @@
/*
Copyright 2023 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.
*/
// +k8s:deepcopy-gen=package
package config // import "k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config"

View File

@@ -0,0 +1,26 @@
/*
Copyright 2023 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 config
// ValidatingAdmissionPolicyStatusControllerConfiguration contains elements describing ValidatingAdmissionPolicyStatusController.
type ValidatingAdmissionPolicyStatusControllerConfiguration struct {
// ConcurrentPolicySyncs is the number of policy objects that are
// allowed to sync concurrently. Larger number = quicker type checking,
// but more CPU (and network) load.
// The default value is 5.
ConcurrentPolicySyncs int32
}

View File

@@ -0,0 +1,40 @@
/*
Copyright 2023 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 v1alpha1
import (
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/kube-controller-manager/config/v1alpha1"
"k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config"
)
// Important! The public back-and-forth conversion functions for the types in this package
// need to be manually exposed like this in order for other packages that reference
// this package to be able to call these conversion functions in an autogenerated manner.
// TODO: Fix the bug in conversion-gen so it automatically discovers these Convert_* functions
// in autogenerated code as well. This is a limitation that affects all controller configurations
// This issue was introduced in #72800
// Convert_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration_To_config_ValidatingAdmissionPolicyStatusControllerConfiguration is an autogenerated conversion function.
func Convert_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration_To_config_ValidatingAdmissionPolicyStatusControllerConfiguration(in *v1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration, out *config.ValidatingAdmissionPolicyStatusControllerConfiguration, s conversion.Scope) error {
return autoConvert_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration_To_config_ValidatingAdmissionPolicyStatusControllerConfiguration(in, out, s)
}
// Convert_config_ValidatingAdmissionPolicyStatusControllerConfiguration_To_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration is an autogenerated conversion function.
func Convert_config_ValidatingAdmissionPolicyStatusControllerConfiguration_To_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration(in *config.ValidatingAdmissionPolicyStatusControllerConfiguration, out *v1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration, s conversion.Scope) error {
return autoConvert_config_ValidatingAdmissionPolicyStatusControllerConfiguration_To_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration(in, out, s)
}

View File

@@ -0,0 +1,36 @@
/*
Copyright 2023 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 v1alpha1
import (
kubectrlmgrconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
)
// RecommendedDefaultValidatingAdmissionPolicyStatusControllerConfiguration defaults a pointer to a
// ValidatingAdmissionPolicyStatusControllerConfiguration struct. This will set the recommended default
// values, but they may be subject to change between API versions. This function
// is intentionally not registered in the scheme as a "normal" `SetDefaults_Foo`
// function to allow consumers of this type to set whatever defaults for their
// embedded configs. Forcing consumers to use these defaults would be problematic
// as defaulting in the scheme is done as part of the conversion, and there would
// be no easy way to opt-out. Instead, if you want to use this defaulting method
// run it in your wrapper struct of this type in its `SetDefaults_` method.
func RecommendedDefaultValidatingAdmissionPolicyStatusControllerConfiguration(obj *kubectrlmgrconfigv1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration) {
if obj.ConcurrentPolicySyncs == 0 {
obj.ConcurrentPolicySyncs = 5
}
}

View File

@@ -0,0 +1,31 @@
/*
Copyright 2023 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 v1alpha1
import (
"testing"
kubectrlmgrconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
)
func TestRecommendedDefaultValidatingAdmissionPolicyStatusControllerConfiguration(t *testing.T) {
config := new(kubectrlmgrconfigv1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration)
RecommendedDefaultValidatingAdmissionPolicyStatusControllerConfiguration(config)
if config.ConcurrentPolicySyncs != 5 {
t.Errorf("incorrect default value, expected 5 but got %v", config.ConcurrentPolicySyncs)
}
}

View File

@@ -0,0 +1,21 @@
/*
Copyright 2023 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.
*/
// +k8s:deepcopy-gen=package
// +k8s:conversion-gen=k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config
// +k8s:conversion-gen-external-types=k8s.io/kube-controller-manager/config/v1alpha1
package v1alpha1 // import "k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config/v1alpha1"

View File

@@ -0,0 +1,31 @@
/*
Copyright 2023 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 v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
)
var (
// SchemeBuilder is the scheme builder with scheme init functions to run for this API package
SchemeBuilder runtime.SchemeBuilder
// localSchemeBuilder extends the SchemeBuilder instance with the external types. In this package,
// defaulting and conversion init funcs are registered as well.
localSchemeBuilder = &SchemeBuilder
// AddToScheme is a global function that registers this API group & version to a scheme
AddToScheme = localSchemeBuilder.AddToScheme
)

View File

@@ -0,0 +1,92 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by conversion-gen. DO NOT EDIT.
package v1alpha1
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
v1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
config "k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatus/config"
)
func init() {
localSchemeBuilder.Register(RegisterConversions)
}
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(s *runtime.Scheme) error {
if err := s.AddGeneratedConversionFunc((*v1alpha1.GroupResource)(nil), (*v1.GroupResource)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_GroupResource_To_v1_GroupResource(a.(*v1alpha1.GroupResource), b.(*v1.GroupResource), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v1.GroupResource)(nil), (*v1alpha1.GroupResource)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_GroupResource_To_v1alpha1_GroupResource(a.(*v1.GroupResource), b.(*v1alpha1.GroupResource), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*config.ValidatingAdmissionPolicyStatusControllerConfiguration)(nil), (*v1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_ValidatingAdmissionPolicyStatusControllerConfiguration_To_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration(a.(*config.ValidatingAdmissionPolicyStatusControllerConfiguration), b.(*v1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*v1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration)(nil), (*config.ValidatingAdmissionPolicyStatusControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration_To_config_ValidatingAdmissionPolicyStatusControllerConfiguration(a.(*v1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration), b.(*config.ValidatingAdmissionPolicyStatusControllerConfiguration), scope)
}); err != nil {
return err
}
return nil
}
func autoConvert_v1alpha1_GroupResource_To_v1_GroupResource(in *v1alpha1.GroupResource, out *v1.GroupResource, s conversion.Scope) error {
out.Group = in.Group
out.Resource = in.Resource
return nil
}
// Convert_v1alpha1_GroupResource_To_v1_GroupResource is an autogenerated conversion function.
func Convert_v1alpha1_GroupResource_To_v1_GroupResource(in *v1alpha1.GroupResource, out *v1.GroupResource, s conversion.Scope) error {
return autoConvert_v1alpha1_GroupResource_To_v1_GroupResource(in, out, s)
}
func autoConvert_v1_GroupResource_To_v1alpha1_GroupResource(in *v1.GroupResource, out *v1alpha1.GroupResource, s conversion.Scope) error {
out.Group = in.Group
out.Resource = in.Resource
return nil
}
// Convert_v1_GroupResource_To_v1alpha1_GroupResource is an autogenerated conversion function.
func Convert_v1_GroupResource_To_v1alpha1_GroupResource(in *v1.GroupResource, out *v1alpha1.GroupResource, s conversion.Scope) error {
return autoConvert_v1_GroupResource_To_v1alpha1_GroupResource(in, out, s)
}
func autoConvert_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration_To_config_ValidatingAdmissionPolicyStatusControllerConfiguration(in *v1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration, out *config.ValidatingAdmissionPolicyStatusControllerConfiguration, s conversion.Scope) error {
out.ConcurrentPolicySyncs = in.ConcurrentPolicySyncs
return nil
}
func autoConvert_config_ValidatingAdmissionPolicyStatusControllerConfiguration_To_v1alpha1_ValidatingAdmissionPolicyStatusControllerConfiguration(in *config.ValidatingAdmissionPolicyStatusControllerConfiguration, out *v1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration, s conversion.Scope) error {
out.ConcurrentPolicySyncs = in.ConcurrentPolicySyncs
return nil
}

View File

@@ -0,0 +1,22 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1alpha1

View File

@@ -0,0 +1,38 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package config
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValidatingAdmissionPolicyStatusControllerConfiguration) DeepCopyInto(out *ValidatingAdmissionPolicyStatusControllerConfiguration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValidatingAdmissionPolicyStatusControllerConfiguration.
func (in *ValidatingAdmissionPolicyStatusControllerConfiguration) DeepCopy() *ValidatingAdmissionPolicyStatusControllerConfiguration {
if in == nil {
return nil
}
out := new(ValidatingAdmissionPolicyStatusControllerConfiguration)
in.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,162 @@
/*
Copyright 2023 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 validatingadmissionpolicystatus
import (
"context"
"fmt"
"time"
"k8s.io/api/admissionregistration/v1alpha1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
admissionregistrationv1alpha1apply "k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1"
informerv1alpha1 "k8s.io/client-go/informers/admissionregistration/v1alpha1"
admissionregistrationv1alpha1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
)
// ControllerName has "Status" in it to differentiate this controller with the other that runs in API server.
const ControllerName = "validatingadmissionpolicy-status"
// Controller is the ValidatingAdmissionPolicy Status controller that reconciles the Status field of each policy object.
// This controller runs type checks against referred types for each policy definition.
type Controller struct {
policyInformer informerv1alpha1.ValidatingAdmissionPolicyInformer
policyQueue workqueue.RateLimitingInterface
policySynced cache.InformerSynced
policyClient admissionregistrationv1alpha1.ValidatingAdmissionPolicyInterface
// typeChecker checks the policy's expressions for type errors.
// Type of params is defined in policy.Spec.ParamsKind
// Types of object are calculated from policy.Spec.MatchingConstraints
typeChecker *validatingadmissionpolicy.TypeChecker
}
func (c *Controller) Run(ctx context.Context, workers int) {
defer utilruntime.HandleCrash()
if !cache.WaitForNamedCacheSync(ControllerName, ctx.Done(), c.policySynced) {
return
}
defer c.policyQueue.ShutDown()
for i := 0; i < workers; i++ {
go wait.UntilWithContext(ctx, c.runWorker, time.Second)
}
<-ctx.Done()
}
func NewController(policyInformer informerv1alpha1.ValidatingAdmissionPolicyInformer, policyClient admissionregistrationv1alpha1.ValidatingAdmissionPolicyInterface, typeChecker *validatingadmissionpolicy.TypeChecker) (*Controller, error) {
c := &Controller{
policyInformer: policyInformer,
policyQueue: workqueue.NewRateLimitingQueueWithConfig(workqueue.DefaultControllerRateLimiter(), workqueue.RateLimitingQueueConfig{Name: ControllerName}),
policyClient: policyClient,
typeChecker: typeChecker,
}
reg, err := policyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
c.enqueuePolicy(obj)
},
UpdateFunc: func(oldObj, newObj interface{}) {
c.enqueuePolicy(newObj)
},
})
if err != nil {
return nil, err
}
c.policySynced = reg.HasSynced
return c, nil
}
func (c *Controller) enqueuePolicy(policy any) {
if policy, ok := policy.(*v1alpha1.ValidatingAdmissionPolicy); ok {
// policy objects are cluster-scoped, no point include its namespace.
key := policy.ObjectMeta.Name
if key == "" {
utilruntime.HandleError(fmt.Errorf("cannot get name of object %v", policy))
}
c.policyQueue.Add(key)
}
}
func (c *Controller) runWorker(ctx context.Context) {
for c.processNextWorkItem(ctx) {
}
}
func (c *Controller) processNextWorkItem(ctx context.Context) bool {
key, shutdown := c.policyQueue.Get()
if shutdown {
return false
}
defer c.policyQueue.Done(key)
err := func() error {
key, ok := key.(string)
if !ok {
return fmt.Errorf("expect a string but got %v", key)
}
policy, err := c.policyInformer.Lister().Get(key)
if err != nil {
if kerrors.IsNotFound(err) {
// If not found, the policy is being deleting, do nothing.
return nil
}
return err
}
return c.reconcile(ctx, policy)
}()
if err == nil {
c.policyQueue.Forget(key)
return true
}
utilruntime.HandleError(err)
c.policyQueue.AddRateLimited(key)
return true
}
func (c *Controller) reconcile(ctx context.Context, policy *v1alpha1.ValidatingAdmissionPolicy) error {
if policy == nil {
return nil
}
if policy.Generation <= policy.Status.ObservedGeneration {
return nil
}
warnings := c.typeChecker.Check(policy)
warningsConfig := make([]*admissionregistrationv1alpha1apply.ExpressionWarningApplyConfiguration, 0, len(warnings))
for _, warning := range warnings {
warningsConfig = append(warningsConfig, admissionregistrationv1alpha1apply.ExpressionWarning().
WithFieldRef(warning.FieldRef).
WithWarning(warning.Warning))
}
applyConfig := admissionregistrationv1alpha1apply.ValidatingAdmissionPolicy(policy.Name).
WithStatus(admissionregistrationv1alpha1apply.ValidatingAdmissionPolicyStatus().
WithObservedGeneration(policy.Generation).
WithTypeChecking(admissionregistrationv1alpha1apply.TypeChecking().
WithExpressionWarnings(warningsConfig...)))
_, err := c.policyClient.ApplyStatus(ctx, applyConfig, metav1.ApplyOptions{FieldManager: ControllerName, Force: true})
return err
}

View File

@@ -0,0 +1,224 @@
/*
Copyright 2023 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 validatingadmissionpolicystatus
import (
"context"
"strings"
"testing"
"time"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/kubernetes/pkg/generated/openapi"
)
func TestTypeChecking(t *testing.T) {
for _, tc := range []struct {
name string
policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy
assertFieldRef func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) // warning.fieldRef
assertWarnings func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) // warning.warning
}{
{
name: "deployment with correct expression",
policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.spec.replicas > 1",
},
}, makePolicy("replicated-deployment"))),
assertFieldRef: toHaveLengthOf(0),
assertWarnings: toHaveLengthOf(0),
},
{
name: "deployment with type confusion",
policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.spec.replicas < 100", // this one passes
},
{
Expression: "object.spec.replicas > '1'", // '1' should be int
},
}, makePolicy("confused-deployment"))),
assertFieldRef: toBe("spec.validations[1].expression"),
assertWarnings: toHaveSubstring(`found no matching overload for '_>_' applied to '(int, string)'`),
},
{
name: "two expressions different type checking errors",
policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.spec.nonExistingFirst > 1",
},
{
Expression: "object.spec.replicas > '1'", // '1' should be int
},
}, makePolicy("confused-deployment"))),
assertFieldRef: toBe("spec.validations[0].expression", "spec.validations[1].expression"),
assertWarnings: toHaveSubstring(
"undefined field 'nonExistingFirst'",
`found no matching overload for '_>_' applied to '(int, string)'`,
),
},
{
name: "one expression, two warnings",
policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.spec.replicas < 100", // this one passes
},
{
Expression: "object.spec.replicas > '1' && object.spec.nonExisting == 1",
},
}, makePolicy("confused-deployment"))),
assertFieldRef: toBe("spec.validations[1].expression"),
assertWarnings: toHaveMultipleSubstrings([]string{"undefined field 'nonExisting'", `found no matching overload for '_>_' applied to '(int, string)'`}),
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
policy := tc.policy.DeepCopy()
policy.ObjectMeta.Generation = 1 // fake storage does not do this automatically
client := fake.NewSimpleClientset(policy)
informerFactory := informers.NewSharedInformerFactory(client, 0)
typeChecker := &validatingadmissionpolicy.TypeChecker{
SchemaResolver: resolver.NewDefinitionsSchemaResolver(scheme.Scheme, openapi.GetOpenAPIDefinitions),
RestMapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme),
}
controller, err := NewController(
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies(),
client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies(),
typeChecker,
)
if err != nil {
t.Fatalf("cannot create controller: %v", err)
}
go informerFactory.Start(ctx.Done())
go controller.Run(ctx, 1)
err = wait.PollUntilContextCancel(ctx, time.Second, false, func(ctx context.Context) (done bool, err error) {
name := policy.Name
// wait until the typeChecking is set, which means the type checking
// is complete.
updated, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{})
if err != nil {
return false, err
}
if updated.Status.TypeChecking != nil {
policy = updated
return true, nil
}
return false, nil
})
if err != nil {
t.Fatal(err)
}
tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t)
tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t)
if err != nil {
t.Fatalf("failed to initialize controller: %v", err)
}
})
}
}
func toBe(expected ...string) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if len(expected) != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings))
}
for i := range expected {
if expected[i] != warnings[i].FieldRef {
t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef)
}
}
}
}
func toHaveSubstring(substrings ...string) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if len(substrings) != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
}
for i := range substrings {
if !strings.Contains(warnings[i].Warning, substrings[i]) {
t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
}
}
}
}
func toHaveMultipleSubstrings(substrings ...[]string) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if len(substrings) != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
}
for i, expectedSubstrings := range substrings {
for _, s := range expectedSubstrings {
if !strings.Contains(warnings[i].Warning, s) {
t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
}
}
}
}
}
func toHaveLengthOf(n int) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if n != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings))
}
}
}
func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"*",
},
Rule: admissionregistrationv1.Rule{
APIGroups: groups,
APIVersions: versions,
Resources: resources,
},
},
},
},
}
return policy
}
func withValidations(validations []admissionregistrationv1alpha1.Validation, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.Validations = validations
return policy
}
func makePolicy(name string) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
return &admissionregistrationv1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: name},
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,6 @@ import (
utilwait "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
webhookinit "k8s.io/apiserver/pkg/admission/plugin/webhook/initializer"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
genericapiserver "k8s.io/apiserver/pkg/server"
egressselector "k8s.io/apiserver/pkg/server/egressselector"
"k8s.io/apiserver/pkg/util/webhook"
@@ -48,7 +47,7 @@ type Config struct {
}
// New sets up the plugins and admission start hooks needed for admission
func (c *Config) New(proxyTransport *http.Transport, egressSelector *egressselector.EgressSelector, serviceResolver webhook.ServiceResolver, tp trace.TracerProvider, schemaResolver resolver.SchemaResolver) ([]admission.PluginInitializer, genericapiserver.PostStartHookFunc, error) {
func (c *Config) New(proxyTransport *http.Transport, egressSelector *egressselector.EgressSelector, serviceResolver webhook.ServiceResolver, tp trace.TracerProvider) ([]admission.PluginInitializer, genericapiserver.PostStartHookFunc, error) {
webhookAuthResolverWrapper := webhook.NewDefaultAuthenticationInfoResolverWrapper(proxyTransport, egressSelector, c.LoopbackClientConfig, tp)
webhookPluginInitializer := webhookinit.NewPluginInitializer(webhookAuthResolverWrapper, serviceResolver)
@@ -70,7 +69,6 @@ func (c *Config) New(proxyTransport *http.Transport, egressSelector *egressselec
cloudConfig,
discoveryRESTMapper,
quotainstall.NewQuotaConfigurationForAdmission(),
schemaResolver,
)
admissionPostStartHook := func(context genericapiserver.PostStartHookContext) error {

View File

@@ -20,7 +20,6 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
quota "k8s.io/apiserver/pkg/quota/v1"
)
@@ -36,7 +35,6 @@ type PluginInitializer struct {
cloudConfig []byte
restMapper meta.RESTMapper
quotaConfiguration quota.Configuration
schemaResolver resolver.SchemaResolver
}
var _ admission.PluginInitializer = &PluginInitializer{}
@@ -48,13 +46,11 @@ func NewPluginInitializer(
cloudConfig []byte,
restMapper meta.RESTMapper,
quotaConfiguration quota.Configuration,
schemaResolver resolver.SchemaResolver,
) *PluginInitializer {
return &PluginInitializer{
cloudConfig: cloudConfig,
restMapper: restMapper,
quotaConfiguration: quotaConfiguration,
schemaResolver: schemaResolver,
}
}
@@ -72,8 +68,4 @@ func (i *PluginInitializer) Initialize(plugin admission.Interface) {
if wants, ok := plugin.(initializer.WantsQuotaConfiguration); ok {
wants.SetQuotaConfiguration(i.quotaConfiguration)
}
if wants, ok := plugin.(initializer.WantsSchemaResolver); ok {
wants.SetSchemaResolver(i.schemaResolver)
}
}

View File

@@ -49,7 +49,7 @@ func (p *WantsCloudConfigAdmissionPlugin) SetCloudConfig(cloudConfig []byte) {
func TestCloudConfigAdmissionPlugin(t *testing.T) {
cloudConfig := []byte("cloud-configuration")
initializer := NewPluginInitializer(cloudConfig, nil, nil, nil)
initializer := NewPluginInitializer(cloudConfig, nil, nil)
wantsCloudConfigAdmission := &WantsCloudConfigAdmissionPlugin{}
initializer.Initialize(wantsCloudConfigAdmission)
@@ -94,7 +94,7 @@ func (p *WantsRESTMapperAdmissionPlugin) SetRESTMapper(mapper meta.RESTMapper) {
func TestRESTMapperAdmissionPlugin(t *testing.T) {
mapper := doNothingRESTMapper{}
initializer := NewPluginInitializer(nil, mapper, nil, nil)
initializer := NewPluginInitializer(nil, mapper, nil)
wantsRESTMapperAdmission := &WantsRESTMapperAdmissionPlugin{}
initializer.Initialize(wantsRESTMapperAdmission)
@@ -121,7 +121,7 @@ func (p *WantsQuotaConfigurationAdmissionPlugin) SetQuotaConfiguration(config qu
func TestQuotaConfigurationAdmissionPlugin(t *testing.T) {
config := doNothingQuotaConfiguration{}
initializer := NewPluginInitializer(nil, nil, config, nil)
initializer := NewPluginInitializer(nil, nil, config)
wantsQuotaConfigurationAdmission := &WantsQuotaConfigurationAdmissionPlugin{}
initializer.Initialize(wantsQuotaConfigurationAdmission)

View File

@@ -139,7 +139,7 @@ func newGCPermissionsEnforcement() (*gcPermissionsEnforcement, error) {
return nil, fmt.Errorf("unexpected error while constructing resource list from fake discovery client: %v", err)
}
restMapper := restmapper.NewDiscoveryRESTMapper(restMapperRes)
pluginInitializer := kubeadmission.NewPluginInitializer(nil, restMapper, nil, nil)
pluginInitializer := kubeadmission.NewPluginInitializer(nil, restMapper, nil)
initializersChain := admission.PluginInitializers{}
initializersChain = append(initializersChain, genericPluginInitializer)
initializersChain = append(initializersChain, pluginInitializer)

View File

@@ -111,7 +111,7 @@ func createHandlerWithConfig(kubeClient kubernetes.Interface, informerFactory in
initializers := admission.PluginInitializers{
genericadmissioninitializer.New(kubeClient, nil, informerFactory, nil, nil, stopCh),
kubeapiserveradmission.NewPluginInitializer(nil, nil, quotaConfiguration, nil),
kubeapiserveradmission.NewPluginInitializer(nil, nil, quotaConfiguration),
}
initializers.Initialize(handler)

View File

@@ -441,6 +441,18 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding)
eventsRule(),
},
})
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.ValidatingAdmissionPolicy) {
addControllerRole(&controllerRoles, &controllerRoleBindings, rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{Name: saRolePrefix + "validatingadmissionpolicy-status-controller"},
Rules: []rbacv1.PolicyRule{
rbacv1helpers.NewRule("get", "list", "watch").Groups(admissionRegistrationGroup).
Resources("validatingadmissionpolicies").RuleOrDie(),
rbacv1helpers.NewRule("get", "patch", "update").Groups(admissionRegistrationGroup).
Resources("validatingadmissionpolicies/status").RuleOrDie(),
eventsRule(),
},
})
}
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) &&
utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) {
addControllerRole(&controllerRoles, &controllerRoleBindings, rbacv1.ClusterRole{

View File

@@ -42,26 +42,27 @@ var (
)
const (
legacyGroup = ""
appsGroup = "apps"
authenticationGroup = "authentication.k8s.io"
authorizationGroup = "authorization.k8s.io"
autoscalingGroup = "autoscaling"
batchGroup = "batch"
certificatesGroup = "certificates.k8s.io"
coordinationGroup = "coordination.k8s.io"
discoveryGroup = "discovery.k8s.io"
extensionsGroup = "extensions"
policyGroup = "policy"
rbacGroup = "rbac.authorization.k8s.io"
resourceGroup = "resource.k8s.io"
storageGroup = "storage.k8s.io"
resMetricsGroup = "metrics.k8s.io"
customMetricsGroup = "custom.metrics.k8s.io"
externalMetricsGroup = "external.metrics.k8s.io"
networkingGroup = "networking.k8s.io"
eventsGroup = "events.k8s.io"
internalAPIServerGroup = "internal.apiserver.k8s.io"
legacyGroup = ""
appsGroup = "apps"
authenticationGroup = "authentication.k8s.io"
authorizationGroup = "authorization.k8s.io"
autoscalingGroup = "autoscaling"
batchGroup = "batch"
certificatesGroup = "certificates.k8s.io"
coordinationGroup = "coordination.k8s.io"
discoveryGroup = "discovery.k8s.io"
extensionsGroup = "extensions"
policyGroup = "policy"
rbacGroup = "rbac.authorization.k8s.io"
resourceGroup = "resource.k8s.io"
storageGroup = "storage.k8s.io"
resMetricsGroup = "metrics.k8s.io"
customMetricsGroup = "custom.metrics.k8s.io"
externalMetricsGroup = "external.metrics.k8s.io"
networkingGroup = "networking.k8s.io"
eventsGroup = "events.k8s.io"
internalAPIServerGroup = "internal.apiserver.k8s.io"
admissionRegistrationGroup = "admissionregistration.k8s.io"
)
func addDefaultMetadata(obj runtime.Object) {

View File

@@ -24,7 +24,6 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/component-base/featuregate"
@@ -74,7 +73,6 @@ type celAdmissionPlugin struct {
dynamicClient dynamic.Interface
stopCh <-chan struct{}
authorizer authorizer.Authorizer
schemaResolver resolver.SchemaResolver
}
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
@@ -83,7 +81,6 @@ var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
var _ initializer.WantsAuthorizer = &celAdmissionPlugin{}
var _ initializer.WantsSchemaResolver = &celAdmissionPlugin{}
var _ admission.InitializationValidator = &celAdmissionPlugin{}
var _ admission.ValidationInterface = &celAdmissionPlugin{}
@@ -116,11 +113,6 @@ func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) {
c.authorizer = authorizer
}
func (c *celAdmissionPlugin) SetSchemaResolver(resolver resolver.SchemaResolver) {
c.schemaResolver = resolver
}
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
c.enabled = true
@@ -154,7 +146,7 @@ func (c *celAdmissionPlugin) ValidateInitialization() error {
if c.authorizer == nil {
return errors.New("missing authorizer")
}
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.schemaResolver /* (optional) */, c.dynamicClient, c.authorizer)
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient, c.authorizer)
if err := c.evaluator.ValidateInitialization(); err != nil {
return err
}

View File

@@ -25,8 +25,6 @@ import (
"sync/atomic"
"time"
"k8s.io/klog/v2"
"k8s.io/api/admissionregistration/v1alpha1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
@@ -44,12 +42,12 @@ import (
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/apiserver/pkg/warning"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
)
var _ CELPolicyEvaluator = &celAdmissionController{}
@@ -128,21 +126,15 @@ func NewAdmissionController(
informerFactory informers.SharedInformerFactory,
client kubernetes.Interface,
restMapper meta.RESTMapper,
schemaResolver resolver.SchemaResolver,
dynamicClient dynamic.Interface,
authz authorizer.Authorizer,
) CELPolicyEvaluator {
var typeChecker *TypeChecker
if schemaResolver != nil {
typeChecker = &TypeChecker{schemaResolver: schemaResolver, restMapper: restMapper}
}
return &celAdmissionController{
definitions: atomic.Value{},
policyController: newPolicyController(
restMapper,
client,
dynamicClient,
typeChecker,
cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())),
NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](

View File

@@ -27,10 +27,8 @@ import (
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
celmetrics "k8s.io/apiserver/pkg/admission/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
@@ -61,11 +59,6 @@ type policyController struct {
newValidator
// The TypeCheck checks the policy's expressions for type errors.
// Type of params is defined in policy.Spec.ParamsKind
// Types of object are calculated from policy.Spec.MatchingConstraints
typeChecker *TypeChecker
// Lock which protects:
// - cachedPolicies
// - paramCRDControllers
@@ -103,7 +96,6 @@ func newPolicyController(
restMapper meta.RESTMapper,
client kubernetes.Interface,
dynamicClient dynamic.Interface,
typeChecker *TypeChecker,
filterCompiler cel.FilterCompiler,
matcher Matcher,
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
@@ -112,7 +104,6 @@ func newPolicyController(
res := &policyController{}
*res = policyController{
filterCompiler: filterCompiler,
typeChecker: typeChecker,
definitionInfo: make(map[namespacedName]*definitionInfo),
bindingInfos: make(map[namespacedName]*bindingInfo),
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
@@ -174,12 +165,6 @@ func (c *policyController) reconcilePolicyDefinition(namespace, name string, def
c.mutex.Lock()
defer c.mutex.Unlock()
err := c.reconcilePolicyDefinitionSpec(namespace, name, definition)
if err != nil {
return err
}
if c.typeChecker != nil {
err = c.reconcilePolicyStatus(namespace, name, definition)
}
return err
}
@@ -438,30 +423,6 @@ func (c *policyController) reconcilePolicyBinding(namespace, name string, bindin
return nil
}
func (c *policyController) reconcilePolicyStatus(namespace, name string, definition *v1alpha1.ValidatingAdmissionPolicy) error {
if definition != nil && definition.Status.ObservedGeneration < definition.Generation {
st := c.calculatePolicyStatus(definition)
newDefinition := definition.DeepCopy()
newDefinition.Status = *st
_, err := c.client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().UpdateStatus(c.context, newDefinition, metav1.UpdateOptions{})
if err != nil {
// ignore error when the controller is not able to
// mutate the definition, and to avoid infinite requeue.
utilruntime.HandleError(err)
}
}
return nil
}
func (c *policyController) calculatePolicyStatus(definition *v1alpha1.ValidatingAdmissionPolicy) *v1alpha1.ValidatingAdmissionPolicyStatus {
expressionWarnings := c.typeChecker.Check(definition)
// modifying a deepcopy of the original status, preserving unrelated existing data
status := definition.Status.DeepCopy()
status.ObservedGeneration = definition.Generation
status.TypeChecking = &v1alpha1.TypeChecking{ExpressionWarnings: expressionWarnings}
return status
}
func (c *policyController) reconcileParams(namespace, name string, params runtime.Object) error {
// Do nothing.
// When we add informational type checking we will need to compile in the

View File

@@ -44,8 +44,8 @@ import (
const maxTypesToCheck = 10
type TypeChecker struct {
schemaResolver resolver.SchemaResolver
restMapper meta.RESTMapper
SchemaResolver resolver.SchemaResolver
RestMapper meta.RESTMapper
}
// TypeCheckingContext holds information about the policy being type-checked.
@@ -196,7 +196,7 @@ func (c *TypeChecker) declType(gvk schema.GroupVersionKind) (*apiservercel.DeclT
if gvk.Empty() {
return nil, nil
}
s, err := c.schemaResolver.ResolveSchema(gvk)
s, err := c.SchemaResolver.ResolveSchema(gvk)
if err != nil {
return nil, err
}
@@ -266,7 +266,7 @@ func (c *TypeChecker) typesToCheck(p *v1alpha1.ValidatingAdmissionPolicy) []sche
Version: version,
Resource: resource,
}
resolved, err := c.restMapper.KindsFor(gvr)
resolved, err := c.RestMapper.KindsFor(gvr)
if err != nil {
continue
}

View File

@@ -435,8 +435,8 @@ func buildTypeChecker(schemaToReturn *spec.Schema) *TypeChecker {
restMapper.Add(must3(scheme.ObjectKinds(&appsv1.Deployment{}))[0], meta.RESTScopeRoot)
return &TypeChecker{
schemaResolver: &fakeSchemaResolver{schemaToReturn: schemaToReturn},
restMapper: restMapper,
SchemaResolver: &fakeSchemaResolver{schemaToReturn: schemaToReturn},
RestMapper: restMapper,
}
}

View File

@@ -165,6 +165,9 @@ type KubeControllerManagerConfiguration struct {
// TTLAfterFinishedControllerConfiguration holds configuration for
// TTLAfterFinishedController related features.
TTLAfterFinishedController TTLAfterFinishedControllerConfiguration
// ValidatingAdmissionPolicyStatusControllerConfiguration holds configuration for
// ValidatingAdmissionPolicyStatusController related features.
ValidatingAdmissionPolicyStatusController ValidatingAdmissionPolicyStatusControllerConfiguration
}
// AttachDetachControllerConfiguration contains elements describing AttachDetachController.
@@ -481,3 +484,12 @@ type TTLAfterFinishedControllerConfiguration struct {
// allowed to sync concurrently.
ConcurrentTTLSyncs int32
}
// ValidatingAdmissionPolicyStatusControllerConfiguration contains elements describing ValidatingAdmissionPolicyStatusController.
type ValidatingAdmissionPolicyStatusControllerConfiguration struct {
// ConcurrentPolicySyncs is the number of policy objects that are
// allowed to sync concurrently. Larger number = quicker type checking,
// but more CPU (and network) load.
// The default value is 5.
ConcurrentPolicySyncs int32
}

View File

@@ -322,6 +322,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa
out.SAController = in.SAController
out.ServiceController = in.ServiceController
out.TTLAfterFinishedController = in.TTLAfterFinishedController
out.ValidatingAdmissionPolicyStatusController = in.ValidatingAdmissionPolicyStatusController
return
}
@@ -569,6 +570,22 @@ func (in *TTLAfterFinishedControllerConfiguration) DeepCopy() *TTLAfterFinishedC
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValidatingAdmissionPolicyStatusControllerConfiguration) DeepCopyInto(out *ValidatingAdmissionPolicyStatusControllerConfiguration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValidatingAdmissionPolicyStatusControllerConfiguration.
func (in *ValidatingAdmissionPolicyStatusControllerConfiguration) DeepCopy() *ValidatingAdmissionPolicyStatusControllerConfiguration {
if in == nil {
return nil
}
out := new(ValidatingAdmissionPolicyStatusControllerConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VolumeConfiguration) DeepCopyInto(out *VolumeConfiguration) {
*out = *in

View File

@@ -22,6 +22,7 @@ import (
"time"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
@@ -112,6 +113,81 @@ var _ = SIGDescribe("ValidatingAdmissionPolicy [Privileged:ClusterAdmin][Alpha][
framework.ExpectNoError(err, "create non-replicated ReplicaSet")
})
})
ginkgo.It("should type check validation expressions", func(ctx context.Context) {
var policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy
ginkgo.By("creating the policy with correct types", func() {
policy = newValidatingAdmissionPolicyBuilder(f.UniqueName+".correct-policy.example.com").
MatchUniqueNamespace(f.UniqueName).
StartResourceRule().
MatchResource([]string{"apps"}, []string{"v1"}, []string{"deployments"}).
EndResourceRule().
WithValidation(admissionregistrationv1alpha1.Validation{
Expression: "object.spec.replicas > 1",
}).
Build()
var err error
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{})
framework.ExpectNoError(err, "create policy")
ginkgo.DeferCleanup(func(ctx context.Context, name string) error {
return client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Delete(ctx, name, metav1.DeleteOptions{})
}, policy.Name)
})
ginkgo.By("waiting for the type check to finish without any warnings", func() {
err := wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(ctx, policy.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if policy.Status.TypeChecking != nil { // non-nil TypeChecking indicates its completion
return true, nil
}
return false, nil
})
framework.ExpectNoError(err, "wait for type checking")
gomega.Expect(policy.Status.TypeChecking.ExpressionWarnings).To(gomega.BeEmpty())
})
ginkgo.By("creating the policy with type confusion", func() {
policy = newValidatingAdmissionPolicyBuilder(f.UniqueName+".confused-policy.example.com").
MatchUniqueNamespace(f.UniqueName).
StartResourceRule().
MatchResource([]string{"apps"}, []string{"v1"}, []string{"deployments"}).
EndResourceRule().
WithValidation(admissionregistrationv1alpha1.Validation{
Expression: "object.spec.replicas > '1'", // confusion: int > string
MessageExpression: "'wants replicas > 1, got ' + object.spec.replicas", // confusion: string + int
}).
Build()
var err error
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{})
framework.ExpectNoError(err, "create policy")
ginkgo.DeferCleanup(func(ctx context.Context, name string) error {
return client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Delete(ctx, name, metav1.DeleteOptions{})
}, policy.Name)
})
ginkgo.By("waiting for the type check to finish with warnings", func() {
err := wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(ctx, policy.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if policy.Status.TypeChecking != nil { // non-nil TypeChecking indicates its completion
return true, nil
}
return false, nil
})
framework.ExpectNoError(err, "wait for type checking")
// assert it to contain 2 warnings, first for expression and second for messageExpression
gomega.Expect(policy.Status.TypeChecking.ExpressionWarnings).To(gomega.HaveLen(2))
warning := policy.Status.TypeChecking.ExpressionWarnings[0]
gomega.Expect(warning.FieldRef).To(gomega.Equal("spec.validations[0].expression"))
gomega.Expect(warning.Warning).To(gomega.ContainSubstring("found no matching overload for '_>_' applied to '(int, string)'"))
warning = policy.Status.TypeChecking.ExpressionWarnings[1]
gomega.Expect(warning.FieldRef).To(gomega.Equal("spec.validations[0].messageExpression"))
gomega.Expect(warning.Warning).To(gomega.ContainSubstring("found no matching overload for '_+_' applied to '(string, int)'"))
})
})
})
func createBinding(bindingName string, uniqueLabel string, policyName string) *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding {

View File

@@ -2622,26 +2622,6 @@ func withPolicyExistsLabels(labels []string, policy *admissionregistrationv1alph
return policy
}
func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"*",
},
Rule: admissionregistrationv1.Rule{
APIGroups: groups,
APIVersions: versions,
Resources: resources,
},
},
},
},
}
return policy
}
func withValidations(validations []admissionregistrationv1alpha1.Validation, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.Validations = validations
return policy
@@ -2917,117 +2897,6 @@ rules:
`
)
func TestValidatingAdmissionPolicyTypeChecking(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy
assertFieldRef func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) // warning.fieldRef
assertWarnings func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) // warning.warning
}{
{
name: "deployment with correct expression",
policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.spec.replicas > 1",
},
}, makePolicy("replicated-deployment"))),
assertFieldRef: toHasLengthOf(0),
assertWarnings: toHasLengthOf(0),
},
{
name: "deployment with type confusion",
policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.spec.replicas < 100", // this one passes
},
{
Expression: "object.spec.replicas > '1'", // '1' should be int
},
}, makePolicy("confused-deployment"))),
assertFieldRef: toBe("spec.validations[1].expression"),
assertWarnings: toHasSubstring(`found no matching overload for '_>_' applied to '(int, string)'`),
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
policy, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(ctx, tc.policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
defer client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Delete(context.Background(), policy.Name, metav1.DeleteOptions{})
err = wait.PollImmediateWithContext(ctx, time.Second, time.Minute, func(ctx context.Context) (done bool, err error) {
name := policy.Name
// wait until the typeChecking is set, which means the type checking
// is complete.
updated, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{})
if err != nil {
return false, err
}
if updated.Status.TypeChecking != nil {
policy = updated
return true, nil
}
return false, nil
})
if err != nil {
t.Fatal(err)
}
tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t)
tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t)
})
}
}
func toBe(expected ...string) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if len(expected) != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings))
}
for i := range expected {
if expected[i] != warnings[i].FieldRef {
t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef)
}
}
}
}
func toHasSubstring(substrings ...string) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if len(substrings) != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
}
for i := range substrings {
if !strings.Contains(warnings[i].Warning, substrings[i]) {
t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
}
}
}
}
func toHasLengthOf(n int) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if n != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings))
}
}
}
func TestAuthorizationDecisionCaching(t *testing.T) {
for _, tc := range []struct {
name string