Merge pull request #119409 from alexzielenski/apiserver/policy/vap-tests
Add test cases for ValidatingAdmissionPolicy
This commit is contained in:
@@ -1206,7 +1206,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
|
||||
genericfeatures.APIResponseCompression: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
genericfeatures.ValidatingAdmissionPolicy: {Default: false, PreRelease: featuregate.Alpha},
|
||||
genericfeatures.ValidatingAdmissionPolicy: {Default: false, PreRelease: featuregate.Beta},
|
||||
|
||||
genericfeatures.CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
@@ -141,7 +142,7 @@ func TestCompositedPolicies(t *testing.T) {
|
||||
if costBudget == 0 {
|
||||
costBudget = celconfig.RuntimeCELCostBudget
|
||||
}
|
||||
result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, nil, costBudget)
|
||||
result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, v1.GroupVersionResource(tc.attributes.GetResource()), v1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, nil, costBudget)
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Fatalf("failed evaluation: %v", err)
|
||||
}
|
||||
|
||||
@@ -253,10 +253,13 @@ func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.Versione
|
||||
}
|
||||
|
||||
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
|
||||
func CreateAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionRequest {
|
||||
// FIXME: how to get resource GVK, GVR and subresource?
|
||||
gvk := attr.GetKind()
|
||||
gvr := attr.GetResource()
|
||||
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
|
||||
// Attempting to use same logic as webhook for constructing resource
|
||||
// GVK, GVR, subresource
|
||||
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
|
||||
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
|
||||
gvk := equivalentKind
|
||||
gvr := equivalentGVR
|
||||
subresource := attr.GetSubresource()
|
||||
|
||||
requestGVK := attr.GetKind()
|
||||
|
||||
@@ -787,7 +787,7 @@ func TestFilter(t *testing.T) {
|
||||
|
||||
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
|
||||
ctx := context.TODO()
|
||||
evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, CreateNamespaceObject(tc.namespaceObject), celconfig.RuntimeCELCostBudget)
|
||||
evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, CreateNamespaceObject(tc.namespaceObject), celconfig.RuntimeCELCostBudget)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -933,7 +933,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
||||
}
|
||||
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
|
||||
ctx := context.TODO()
|
||||
evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, nil, tc.testRuntimeCELCostBudget)
|
||||
evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, nil, tc.testRuntimeCELCostBudget)
|
||||
if tc.exceedBudget && err == nil {
|
||||
t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil")
|
||||
}
|
||||
|
||||
@@ -318,10 +318,10 @@ var _ Validator = &fakeValidator{}
|
||||
|
||||
type fakeValidator struct {
|
||||
validationFilter, auditAnnotationFilter, messageFilter *fakeFilter
|
||||
ValidateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
|
||||
ValidateFunc func(ctx context.Context, matchResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
|
||||
}
|
||||
|
||||
func (f *fakeValidator) RegisterDefinition(definition *v1beta1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult) {
|
||||
func (f *fakeValidator) RegisterDefinition(definition *v1beta1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, matchResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult) {
|
||||
//Key must be something that we can decipher from the inputs to Validate so using message which will be on the validationCondition object of evalResult
|
||||
var key string
|
||||
if len(definition.Spec.Validations) > 0 {
|
||||
@@ -338,8 +338,8 @@ func (f *fakeValidator) RegisterDefinition(definition *v1beta1.ValidatingAdmissi
|
||||
validatorMap[key] = f
|
||||
}
|
||||
|
||||
func (f *fakeValidator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return f.ValidateFunc(ctx, versionedAttr, versionedParams, namespace, runtimeCELCostBudget, authz)
|
||||
func (f *fakeValidator) Validate(ctx context.Context, matchResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return f.ValidateFunc(ctx, matchResource, versionedAttr, versionedParams, namespace, runtimeCELCostBudget, authz)
|
||||
}
|
||||
|
||||
var _ Matcher = &fakeMatcher{}
|
||||
@@ -390,18 +390,18 @@ func (f *fakeMatcher) RegisterBinding(binding *v1beta1.ValidatingAdmissionPolicy
|
||||
|
||||
// Matches says whether this policy definition matches the provided admission
|
||||
// resource request
|
||||
func (f *fakeMatcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) {
|
||||
func (f *fakeMatcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
|
||||
namespace, name := definition.Namespace, definition.Name
|
||||
key := namespacedName{
|
||||
name: name,
|
||||
namespace: namespace,
|
||||
}
|
||||
if fun, ok := f.DefinitionMatchFuncs[key]; ok {
|
||||
return fun(definition, a), a.GetKind(), nil
|
||||
return fun(definition, a), a.GetResource(), a.GetKind(), nil
|
||||
}
|
||||
|
||||
// Default is match everything
|
||||
return f.DefaultMatch, a.GetKind(), nil
|
||||
return f.DefaultMatch, a.GetResource(), a.GetKind(), nil
|
||||
}
|
||||
|
||||
// Matches says whether this policy definition matches the provided admission
|
||||
@@ -834,7 +834,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -904,7 +904,7 @@ func TestDefinitionDoesntMatch(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1019,7 +1019,7 @@ func TestReconfigureBinding(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1130,7 +1130,7 @@ func TestRemoveDefinition(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1199,7 +1199,7 @@ func TestRemoveBinding(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1309,7 +1309,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1370,7 +1370,7 @@ func TestEmptyParamRef(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
// Versioned params must be nil to pass the test
|
||||
if versionedParams != nil {
|
||||
return ValidateResult{
|
||||
@@ -1447,7 +1447,7 @@ func TestEmptyParamSource(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1549,7 +1549,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator1.RegisterDefinition(&policy1, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator1.RegisterDefinition(&policy1, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
evaluations1.Add(1)
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
@@ -1568,7 +1568,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator2.RegisterDefinition(&policy2, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator2.RegisterDefinition(&policy2, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
evaluations2.Add(1)
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
@@ -1678,7 +1678,7 @@ func TestNativeTypeParam(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
evaluations.Add(1)
|
||||
if _, ok := versionedParams.(*v1.ConfigMap); ok {
|
||||
return ValidateResult{
|
||||
@@ -1760,7 +1760,7 @@ func TestAuditValidationAction(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1831,7 +1831,7 @@ func TestWarnValidationAction(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1890,7 +1890,7 @@ func TestAllValidationActions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
{
|
||||
@@ -1977,7 +1977,7 @@ func TestNamespaceParamRefName(t *testing.T) {
|
||||
|
||||
lock := sync.Mutex{}
|
||||
observedParamNamespaces := []string{}
|
||||
validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
@@ -2264,7 +2264,7 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(&policy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(&policy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
observeParam(versionedParams)
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
@@ -2505,7 +2505,7 @@ func TestNamespaceParamRefClusterScopedParamError(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
evaluations.Add(1)
|
||||
if _, ok := versionedParams.(*v1beta1.ValidatingAdmissionPolicy); ok {
|
||||
return ValidateResult{
|
||||
@@ -2570,7 +2570,7 @@ func TestAuditAnnotations(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
o, err := meta.Accessor(versionedParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -244,7 +244,7 @@ func (c *celAdmissionController) Validate(
|
||||
var versionedAttr *admission.VersionedAttributes
|
||||
|
||||
definition := definitionInfo.lastReconciledValue
|
||||
matches, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition)
|
||||
matches, matchResource, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition)
|
||||
if err != nil {
|
||||
// Configuration error.
|
||||
addConfigError(err, definition, nil)
|
||||
@@ -323,7 +323,7 @@ func (c *celAdmissionController) Validate(
|
||||
nested: param,
|
||||
}
|
||||
}
|
||||
validationResults = append(validationResults, bindingInfo.validator.Validate(ctx, versionedAttr, p, namespace, celconfig.RuntimeCELCostBudget, authz))
|
||||
validationResults = append(validationResults, bindingInfo.validator.Validate(ctx, matchResource, versionedAttr, p, namespace, celconfig.RuntimeCELCostBudget, authz))
|
||||
}
|
||||
|
||||
for _, validationResult := range validationResults {
|
||||
|
||||
@@ -86,7 +86,7 @@ type Matcher interface {
|
||||
|
||||
// DefinitionMatches says whether this policy definition matches the provided admission
|
||||
// resource request
|
||||
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error)
|
||||
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error)
|
||||
|
||||
// BindingMatches says whether this policy definition matches the provided admission
|
||||
// resource request
|
||||
@@ -109,5 +109,5 @@ type ValidateResult struct {
|
||||
type Validator interface {
|
||||
// Validate is used to take cel evaluations and convert into decisions
|
||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
|
||||
Validate(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (c *matcher) ValidateInitialization() error {
|
||||
}
|
||||
|
||||
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
|
||||
func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) {
|
||||
func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
|
||||
criteria := matchCriteria{constraints: definition.Spec.MatchConstraints}
|
||||
return c.Matcher.Matches(a, o, &criteria)
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInter
|
||||
return true, nil
|
||||
}
|
||||
criteria := matchCriteria{constraints: binding.Spec.MatchResources}
|
||||
isMatch, _, err := c.Matcher.Matches(a, o, &criteria)
|
||||
isMatch, _, _, err := c.Matcher.Matches(a, o, &criteria)
|
||||
return isMatch, err
|
||||
}
|
||||
|
||||
|
||||
@@ -71,56 +71,60 @@ func (m *Matcher) ValidateInitialization() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Matcher) Matches(attr admission.Attributes, o admission.ObjectInterfaces, criteria MatchCriteria) (bool, schema.GroupVersionKind, error) {
|
||||
func (m *Matcher) Matches(attr admission.Attributes, o admission.ObjectInterfaces, criteria MatchCriteria) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
|
||||
matches, matchNsErr := m.namespaceMatcher.MatchNamespaceSelector(criteria, attr)
|
||||
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
|
||||
if !matches && matchNsErr == nil {
|
||||
return false, schema.GroupVersionKind{}, nil
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
|
||||
}
|
||||
|
||||
matches, matchObjErr := m.objectMatcher.MatchObjectSelector(criteria, attr)
|
||||
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
|
||||
if !matches && matchObjErr == nil {
|
||||
return false, schema.GroupVersionKind{}, nil
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
|
||||
}
|
||||
|
||||
matchResources := criteria.GetMatchResources()
|
||||
matchPolicy := matchResources.MatchPolicy
|
||||
if isExcluded, _, err := matchesResourceRules(matchResources.ExcludeResourceRules, matchPolicy, attr, o); isExcluded || err != nil {
|
||||
return false, schema.GroupVersionKind{}, err
|
||||
if isExcluded, _, _, err := matchesResourceRules(matchResources.ExcludeResourceRules, matchPolicy, attr, o); isExcluded || err != nil {
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, err
|
||||
}
|
||||
|
||||
var (
|
||||
isMatch bool
|
||||
matchResource schema.GroupVersionResource
|
||||
matchKind schema.GroupVersionKind
|
||||
matchErr error
|
||||
)
|
||||
if len(matchResources.ResourceRules) == 0 {
|
||||
isMatch = true
|
||||
matchKind = attr.GetKind()
|
||||
matchResource = attr.GetResource()
|
||||
} else {
|
||||
isMatch, matchKind, matchErr = matchesResourceRules(matchResources.ResourceRules, matchPolicy, attr, o)
|
||||
isMatch, matchResource, matchKind, matchErr = matchesResourceRules(matchResources.ResourceRules, matchPolicy, attr, o)
|
||||
}
|
||||
if matchErr != nil {
|
||||
return false, schema.GroupVersionKind{}, matchErr
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchErr
|
||||
}
|
||||
if !isMatch {
|
||||
return false, schema.GroupVersionKind{}, nil
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
|
||||
}
|
||||
|
||||
// now that we know this applies to this request otherwise, if there were selector errors, return them
|
||||
if matchNsErr != nil {
|
||||
return false, schema.GroupVersionKind{}, matchNsErr
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchNsErr
|
||||
}
|
||||
if matchObjErr != nil {
|
||||
return false, schema.GroupVersionKind{}, matchObjErr
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchObjErr
|
||||
}
|
||||
|
||||
return true, matchKind, nil
|
||||
return true, matchResource, matchKind, nil
|
||||
}
|
||||
|
||||
func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPolicy *v1beta1.MatchPolicyType, attr admission.Attributes, o admission.ObjectInterfaces) (bool, schema.GroupVersionKind, error) {
|
||||
func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPolicy *v1beta1.MatchPolicyType, attr admission.Attributes, o admission.ObjectInterfaces) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
|
||||
matchKind := attr.GetKind()
|
||||
matchResource := attr.GetResource()
|
||||
|
||||
for _, namedRule := range namedRules {
|
||||
rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
|
||||
ruleMatcher := rules.Matcher{
|
||||
@@ -132,14 +136,14 @@ func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPol
|
||||
}
|
||||
// an empty name list always matches
|
||||
if len(namedRule.ResourceNames) == 0 {
|
||||
return true, matchKind, nil
|
||||
return true, matchResource, matchKind, nil
|
||||
}
|
||||
// TODO: GetName() can return an empty string if the user is relying on
|
||||
// the API server to generate the name... figure out what to do for this edge case
|
||||
name := attr.GetName()
|
||||
for _, matchedName := range namedRule.ResourceNames {
|
||||
if name == matchedName {
|
||||
return true, matchKind, nil
|
||||
return true, matchResource, matchKind, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,7 +151,7 @@ func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPol
|
||||
// if match policy is undefined or exact, don't perform fuzzy matching
|
||||
// note that defaulting to fuzzy matching is set by the API
|
||||
if matchPolicy == nil || *matchPolicy == v1beta1.Exact {
|
||||
return false, schema.GroupVersionKind{}, nil
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
|
||||
}
|
||||
|
||||
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
|
||||
@@ -169,11 +173,11 @@ func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPol
|
||||
}
|
||||
matchKind = o.GetEquivalentResourceMapper().KindFor(equivalent, attr.GetSubresource())
|
||||
if matchKind.Empty() {
|
||||
return false, schema.GroupVersionKind{}, fmt.Errorf("unable to convert to %v: unknown kind", equivalent)
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("unable to convert to %v: unknown kind", equivalent)
|
||||
}
|
||||
// an empty name list always matches
|
||||
if len(namedRule.ResourceNames) == 0 {
|
||||
return true, matchKind, nil
|
||||
return true, equivalent, matchKind, nil
|
||||
}
|
||||
|
||||
// TODO: GetName() can return an empty string if the user is relying on
|
||||
@@ -181,12 +185,12 @@ func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPol
|
||||
name := attr.GetName()
|
||||
for _, matchedName := range namedRule.ResourceNames {
|
||||
if name == matchedName {
|
||||
return true, matchKind, nil
|
||||
return true, equivalent, matchKind, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, schema.GroupVersionKind{}, nil
|
||||
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
|
||||
}
|
||||
|
||||
type attrWithResourceOverride struct {
|
||||
|
||||
@@ -100,6 +100,7 @@ func TestMatcher(t *testing.T) {
|
||||
|
||||
expectMatches bool
|
||||
expectMatchKind schema.GroupVersionKind
|
||||
expectMatchResource schema.GroupVersionResource
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
@@ -206,6 +207,7 @@ func TestMatcher(t *testing.T) {
|
||||
}}},
|
||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||
expectMatches: true,
|
||||
expectMatchResource: gvr("extensions", "v1beta1", "deployments"),
|
||||
expectMatchKind: gvk("extensions", "v1beta1", "Deployment"),
|
||||
},
|
||||
{
|
||||
@@ -227,6 +229,7 @@ func TestMatcher(t *testing.T) {
|
||||
}}},
|
||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||
expectMatches: true,
|
||||
expectMatchResource: gvr("apps", "v1beta1", "deployments"),
|
||||
expectMatchKind: gvk("apps", "v1beta1", "Deployment"),
|
||||
},
|
||||
|
||||
@@ -313,6 +316,7 @@ func TestMatcher(t *testing.T) {
|
||||
}}},
|
||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||
expectMatches: true,
|
||||
expectMatchResource: gvr("extensions", "v1beta1", "deployments"),
|
||||
expectMatchKind: gvk("extensions", "v1beta1", "Scale"),
|
||||
},
|
||||
{
|
||||
@@ -334,6 +338,7 @@ func TestMatcher(t *testing.T) {
|
||||
}}},
|
||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||
expectMatches: true,
|
||||
expectMatchResource: gvr("apps", "v1beta1", "deployments"),
|
||||
expectMatchKind: gvk("apps", "v1beta1", "Scale"),
|
||||
},
|
||||
{
|
||||
@@ -382,6 +387,7 @@ func TestMatcher(t *testing.T) {
|
||||
}}},
|
||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("extensions", "v1beta1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||
expectMatches: true,
|
||||
expectMatchResource: gvr("apps", "v1", "deployments"),
|
||||
expectMatchKind: gvk("autoscaling", "v1", "Scale"),
|
||||
},
|
||||
{
|
||||
@@ -536,7 +542,7 @@ func TestMatcher(t *testing.T) {
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
matches, matchKind, err := a.Matches(testcase.attrs, interfaces, &fakeCriteria{matchResources: *testcase.criteria})
|
||||
matches, matchResource, matchKind, err := a.Matches(testcase.attrs, interfaces, &fakeCriteria{matchResources: *testcase.criteria})
|
||||
if err != nil {
|
||||
if len(testcase.expectErr) == 0 {
|
||||
t.Fatal(err)
|
||||
@@ -558,6 +564,22 @@ func TestMatcher(t *testing.T) {
|
||||
if matches != testcase.expectMatches {
|
||||
t.Fatalf("expected matches = %v; got %v", testcase.expectMatches, matches)
|
||||
}
|
||||
|
||||
expectResource := testcase.expectMatchResource
|
||||
if !expectResource.Empty() && !matches {
|
||||
t.Fatalf("expectResource is non-empty, but did not match")
|
||||
} else if expectResource.Empty() {
|
||||
// Test for exact match by default. Tests that expect an equivalent
|
||||
// resource to match should explicitly state so by supplying
|
||||
// expectMatchResource
|
||||
expectResource = testcase.attrs.GetResource()
|
||||
}
|
||||
|
||||
if matches {
|
||||
if matchResource != expectResource {
|
||||
t.Fatalf("expected matchResource %v, got %v", expectResource, matchResource)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
@@ -72,7 +73,7 @@ func auditAnnotationEvaluationForError(f v1.FailurePolicyType) PolicyAuditAnnota
|
||||
// Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
|
||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
|
||||
func (v *validator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
|
||||
var f v1.FailurePolicyType
|
||||
if v.failPolicy == nil {
|
||||
f = v1.Fail
|
||||
@@ -102,7 +103,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
|
||||
|
||||
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: authz}
|
||||
expressionOptionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams}
|
||||
admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes)
|
||||
admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(matchedResource), metav1.GroupVersionKind(versionedAttr.VersionedKind))
|
||||
// Decide which fields are exposed
|
||||
ns := cel.CreateNamespaceObject(namespace)
|
||||
evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, ns, runtimeCELCostBudget)
|
||||
@@ -195,7 +196,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
|
||||
}
|
||||
|
||||
options := cel.OptionalVariableBindings{VersionedParams: versionedParams}
|
||||
auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, namespace, runtimeCELCostBudget)
|
||||
auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, admissionRequest, options, namespace, runtimeCELCostBudget)
|
||||
if err != nil {
|
||||
return ValidateResult{
|
||||
Decisions: []PolicyDecision{
|
||||
|
||||
@@ -893,7 +893,7 @@ func TestValidate(t *testing.T) {
|
||||
if tc.costBudget != 0 {
|
||||
budget = tc.costBudget
|
||||
}
|
||||
validateResult := v.Validate(ctx, fakeVersionedAttr, nil, nil, budget, nil)
|
||||
validateResult := v.Validate(ctx, fakeVersionedAttr.GetResource(), fakeVersionedAttr, nil, nil, budget, nil)
|
||||
|
||||
require.Equal(t, len(validateResult.Decisions), len(tc.policyDecision))
|
||||
|
||||
@@ -945,7 +945,7 @@ func TestContextCanceled(t *testing.T) {
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
cancel()
|
||||
validationResult := v.Validate(ctx, fakeVersionedAttr, nil, nil, celconfig.RuntimeCELCostBudget, nil)
|
||||
validationResult := v.Validate(ctx, fakeVersionedAttr.GetResource(), fakeVersionedAttr, nil, nil, celconfig.RuntimeCELCostBudget, nil)
|
||||
if len(validationResult.Decisions) != 1 || !strings.Contains(validationResult.Decisions[0].Message, "operation interrupted") {
|
||||
t.Errorf("Expected 'operation interrupted' but got %v", validationResult.Decisions)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
@@ -78,7 +79,7 @@ func NewMatcher(filter celplugin.Filter, failPolicy *v1.FailurePolicyType, match
|
||||
|
||||
func (m *matcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, authz authorizer.Authorizer) MatchResult {
|
||||
t := time.Now()
|
||||
evalResults, _, err := m.filter.ForInput(ctx, versionedAttr, celplugin.CreateAdmissionRequest(versionedAttr.Attributes), celplugin.OptionalVariableBindings{
|
||||
evalResults, _, err := m.filter.ForInput(ctx, versionedAttr, celplugin.CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)), celplugin.OptionalVariableBindings{
|
||||
VersionedParams: versionedParams,
|
||||
Authorizer: authz,
|
||||
}, nil, celconfig.RuntimeCELCostBudgetMatchConditions)
|
||||
|
||||
@@ -248,7 +248,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
|
||||
APIServerTracing: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
ValidatingAdmissionPolicy: {Default: false, PreRelease: featuregate.Alpha},
|
||||
ValidatingAdmissionPolicy: {Default: false, PreRelease: featuregate.Beta},
|
||||
|
||||
CustomResourceValidationExpressions: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
|
||||
714
test/integration/apiserver/cel/admission_policy_test.go
Normal file
714
test/integration/apiserver/cel/admission_policy_test.go
Normal file
@@ -0,0 +1,714 @@
|
||||
/*
|
||||
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 cel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
|
||||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
admissionregistrationv1alpha1apis "k8s.io/kubernetes/pkg/apis/admissionregistration/v1alpha1"
|
||||
admissionregistrationv1beta1apis "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/test/integration/etcd"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
beginSentinel = "###___BEGIN_SENTINEL___###"
|
||||
recordSeparator = `###$$$###`
|
||||
)
|
||||
|
||||
// Policy registration helpers
|
||||
var testSpec admissionregistration.ValidatingAdmissionPolicy = admissionregistration.ValidatingAdmissionPolicy{
|
||||
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||
ParamKind: &admissionregistration.ParamKind{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
},
|
||||
Variables: []admissionregistration.Variable{
|
||||
{
|
||||
Name: "shouldFail",
|
||||
Expression: `true`,
|
||||
},
|
||||
{
|
||||
Name: "resourceGroup",
|
||||
Expression: `has(request.resource.group) ? request.resource.group : ""`,
|
||||
},
|
||||
{
|
||||
Name: "resourceVersion",
|
||||
Expression: `has(request.resource.version) ? request.resource.version : ""`,
|
||||
},
|
||||
{
|
||||
Name: "resourceResource",
|
||||
Expression: `has(request.resource.resource) ? request.resource.resource : ""`,
|
||||
},
|
||||
{
|
||||
Name: "subresource",
|
||||
Expression: `has(request.subResource) ? request.subResource : ""`,
|
||||
},
|
||||
{
|
||||
Name: "operation",
|
||||
Expression: `has(request.operation) ? request.operation : ""`,
|
||||
},
|
||||
{
|
||||
Name: "name",
|
||||
Expression: `has(request.name) ? request.name : ""`,
|
||||
},
|
||||
{
|
||||
Name: "namespaceName",
|
||||
Expression: `has(request.namespace) ? request.namespace : ""`,
|
||||
},
|
||||
{
|
||||
Name: "objectExists",
|
||||
Expression: `object != null ? "true" : "false"`,
|
||||
},
|
||||
{
|
||||
Name: "objectAPIVersion",
|
||||
Expression: `(object != null && has(object.apiVersion)) ? object.apiVersion : ""`,
|
||||
},
|
||||
{
|
||||
Name: "objectKind",
|
||||
Expression: `(object != null && has(object.kind)) ? object.kind : ""`,
|
||||
},
|
||||
{
|
||||
Name: "oldObjectExists",
|
||||
Expression: `oldObject != null ? "true" : "false"`,
|
||||
},
|
||||
{
|
||||
Name: "oldObjectAPIVersion",
|
||||
Expression: `(oldObject != null && has(oldObject.apiVersion)) ? oldObject.apiVersion : ""`,
|
||||
},
|
||||
{
|
||||
Name: "oldObjectKind",
|
||||
Expression: `(oldObject != null && has(oldObject.kind)) ? oldObject.kind : ""`,
|
||||
},
|
||||
{
|
||||
Name: "optionsExists",
|
||||
Expression: `(has(request.options) && request.options != null) ? "true" : "false"`,
|
||||
},
|
||||
{
|
||||
Name: "optionsKind",
|
||||
Expression: `(has(request.options) && has(request.options.kind)) ? request.options.kind : ""`,
|
||||
},
|
||||
{
|
||||
Name: "optionsAPIVersion",
|
||||
Expression: `(has(request.options) && has(request.options.apiVersion)) ? request.options.apiVersion : ""`,
|
||||
},
|
||||
{
|
||||
Name: "paramsPhase",
|
||||
Expression: `params.data.phase`,
|
||||
},
|
||||
{
|
||||
Name: "paramsVersion",
|
||||
Expression: `params.data.version`,
|
||||
},
|
||||
{
|
||||
Name: "paramsConvert",
|
||||
Expression: `params.data.convert`,
|
||||
},
|
||||
},
|
||||
// Would be nice to use CEL to create a single map
|
||||
// and stringify it. Unfortunately those library functions
|
||||
// are not yet available, so we must create a map
|
||||
// like so
|
||||
Validations: []admissionregistration.Validation{
|
||||
{
|
||||
// newlines forbidden so use recordSeparator
|
||||
Expression: "!variables.shouldFail",
|
||||
MessageExpression: `"` + beginSentinel + `resourceGroup,resourceVersion,resourceResource,subresource,operation,name,namespace,objectExists,objectKind,objectAPIVersion,oldObjectExists,oldObjectKind,oldObjectAPIVersion,optionsExists,optionsKind,optionsAPIVersion,paramsPhase,paramsVersion,paramsConvert` + recordSeparator + `"+variables.resourceGroup + "," + variables.resourceVersion + "," + variables.resourceResource + "," + variables.subresource + "," + variables.operation + "," + variables.name + "," + variables.namespaceName + "," + variables.objectExists + "," + variables.objectKind + "," + variables.objectAPIVersion + "," + variables.oldObjectExists + "," + variables.oldObjectKind + "," + variables.oldObjectAPIVersion + "," + variables.optionsExists + "," + variables.optionsKind + "," + variables.optionsAPIVersion + "," + variables.paramsPhase + "," + variables.paramsVersion + "," + variables.paramsConvert`,
|
||||
},
|
||||
},
|
||||
MatchConditions: []admissionregistration.MatchCondition{
|
||||
{
|
||||
Name: "testclient-only",
|
||||
Expression: `request.userInfo.username == "` + testClientUsername + `"`,
|
||||
},
|
||||
{
|
||||
Name: "ignore-test-config",
|
||||
Expression: `object == null || !has(object.metadata) || !has(object.metadata.annotations) || !has(object.metadata.annotations.skipMatch) || object.metadata.annotations.skipMatch != "yes"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func createV1beta1ValidatingPolicyAndBinding(client clientset.Interface, convertedRules []admissionregistrationv1beta1.NamedRuleWithOperations) error {
|
||||
denyAction := admissionregistrationv1beta1.DenyAction
|
||||
exact := admissionregistrationv1beta1.Exact
|
||||
equivalent := admissionregistrationv1beta1.Equivalent
|
||||
|
||||
var outSpec admissionregistrationv1beta1.ValidatingAdmissionPolicy
|
||||
if err := admissionregistrationv1beta1apis.Convert_admissionregistration_ValidatingAdmissionPolicy_To_v1beta1_ValidatingAdmissionPolicy(&testSpec, &outSpec, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exactPolicyTemplate := outSpec.DeepCopy()
|
||||
convertedPolicyTemplate := outSpec.DeepCopy()
|
||||
|
||||
exactPolicyTemplate.SetName("test-policy-v1beta1")
|
||||
exactPolicyTemplate.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{
|
||||
ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
|
||||
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
|
||||
Rule: admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
MatchPolicy: &exact,
|
||||
}
|
||||
|
||||
convertedPolicyTemplate.SetName("test-policy-v1beta1-convert")
|
||||
convertedPolicyTemplate.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{
|
||||
ResourceRules: convertedRules,
|
||||
MatchPolicy: &equivalent,
|
||||
}
|
||||
|
||||
exactPolicy, err := client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Create(context.TODO(), exactPolicyTemplate, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
convertPolicy, err := client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Create(context.TODO(), convertedPolicyTemplate, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a param that holds the options for this
|
||||
configuration, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy-v1beta1-param",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"skipMatch": "yes",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"version": "v1beta1",
|
||||
"phase": validation,
|
||||
"convert": "false",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configurationConvert, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy-v1beta1-convert-param",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"skipMatch": "yes",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"version": "v1beta1",
|
||||
"phase": validation,
|
||||
"convert": "true",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), &admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy-v1beta1-binding",
|
||||
},
|
||||
Spec: admissionregistrationv1beta1.ValidatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: exactPolicy.GetName(),
|
||||
ValidationActions: []admissionregistrationv1beta1.ValidationAction{admissionregistrationv1beta1.Warn},
|
||||
ParamRef: &admissionregistrationv1beta1.ParamRef{
|
||||
Name: configuration.GetName(),
|
||||
Namespace: configuration.GetNamespace(),
|
||||
ParameterNotFoundAction: &denyAction,
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), &admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy-v1beta1-convert-binding",
|
||||
},
|
||||
Spec: admissionregistrationv1beta1.ValidatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: convertPolicy.GetName(),
|
||||
ValidationActions: []admissionregistrationv1beta1.ValidationAction{admissionregistrationv1beta1.Warn},
|
||||
ParamRef: &admissionregistrationv1beta1.ParamRef{
|
||||
Name: configurationConvert.GetName(),
|
||||
Namespace: configurationConvert.GetNamespace(),
|
||||
ParameterNotFoundAction: &denyAction,
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createV1alpha1ValidatingPolicyAndBinding(client clientset.Interface, convertedRules []admissionregistrationv1alpha1.NamedRuleWithOperations) error {
|
||||
exact := admissionregistrationv1alpha1.Exact
|
||||
equivalent := admissionregistrationv1alpha1.Equivalent
|
||||
denyAction := admissionregistrationv1alpha1.DenyAction
|
||||
|
||||
var outSpec admissionregistrationv1alpha1.ValidatingAdmissionPolicy
|
||||
if err := admissionregistrationv1alpha1apis.Convert_admissionregistration_ValidatingAdmissionPolicy_To_v1alpha1_ValidatingAdmissionPolicy(&testSpec, &outSpec, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exactPolicyTemplate := outSpec.DeepCopy()
|
||||
convertedPolicyTemplate := outSpec.DeepCopy()
|
||||
|
||||
exactPolicyTemplate.SetName("test-policy-v1alpha1")
|
||||
exactPolicyTemplate.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
|
||||
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
|
||||
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
|
||||
Rule: admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
MatchPolicy: &exact,
|
||||
}
|
||||
|
||||
convertedPolicyTemplate.SetName("test-policy-v1alpha1-convert")
|
||||
convertedPolicyTemplate.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
|
||||
ResourceRules: convertedRules,
|
||||
MatchPolicy: &equivalent,
|
||||
}
|
||||
|
||||
exactPolicy, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), exactPolicyTemplate, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
convertPolicy, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), convertedPolicyTemplate, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a param that holds the options for this
|
||||
configuration, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy-v1alpha1-param",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"skipMatch": "yes",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"version": "v1alpha1",
|
||||
"phase": validation,
|
||||
"convert": "false",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configurationConvert, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy-v1alpha1-convert-param",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"skipMatch": "yes",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"version": "v1alpha1",
|
||||
"phase": validation,
|
||||
"convert": "true",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), &admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy-v1alpha1-binding",
|
||||
},
|
||||
Spec: admissionregistrationv1alpha1.ValidatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: exactPolicy.GetName(),
|
||||
ValidationActions: []admissionregistrationv1alpha1.ValidationAction{admissionregistrationv1alpha1.Warn},
|
||||
ParamRef: &admissionregistrationv1alpha1.ParamRef{
|
||||
Name: configuration.GetName(),
|
||||
Namespace: configuration.GetNamespace(),
|
||||
ParameterNotFoundAction: &denyAction,
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), &admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy-v1alpha1-convert-binding",
|
||||
},
|
||||
Spec: admissionregistrationv1alpha1.ValidatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: convertPolicy.GetName(),
|
||||
ValidationActions: []admissionregistrationv1alpha1.ValidationAction{admissionregistrationv1alpha1.Warn},
|
||||
ParamRef: &admissionregistrationv1alpha1.ParamRef{
|
||||
Name: configurationConvert.GetName(),
|
||||
Namespace: configurationConvert.GetNamespace(),
|
||||
ParameterNotFoundAction: &denyAction,
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This test shows that policy intercepts all requests for all resources,
|
||||
// subresources, verbs, and input versions of policy/binding.
|
||||
//
|
||||
// This test tries to mirror very closely the same test for webhook admission
|
||||
// test/integration/apiserver/admissionwebhook/admission_test.go testWebhookAdmission
|
||||
func TestPolicyAdmission(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APISelfSubjectReview, true)()
|
||||
|
||||
holder := &policyExpectationHolder{
|
||||
holder: holder{
|
||||
t: t,
|
||||
gvrToConvertedGVR: map[metav1.GroupVersionResource]metav1.GroupVersionResource{},
|
||||
gvrToConvertedGVK: map[metav1.GroupVersionResource]schema.GroupVersionKind{},
|
||||
},
|
||||
}
|
||||
|
||||
server := apiservertesting.StartTestServerOrDie(t, nil, []string{
|
||||
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
|
||||
// turn off admission plugins that add finalizers
|
||||
"--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
|
||||
// force enable all resources so we can check storage.
|
||||
"--runtime-config=api/all=true",
|
||||
}, framework.SharedEtcd())
|
||||
defer server.TearDownFn()
|
||||
|
||||
// Create admission policy & binding that match everything
|
||||
clientConfig := server.ClientConfig
|
||||
clientConfig.Impersonate.UserName = testClientUsername
|
||||
clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
|
||||
clientConfig.WarningHandler = holder
|
||||
client, err := clientset.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create CRDs
|
||||
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
|
||||
|
||||
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// gather resources to test
|
||||
dynamicClient, err := dynamic.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, resources, err := client.Discovery().ServerGroupsAndResources()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
|
||||
}
|
||||
|
||||
gvrsToTest := []schema.GroupVersionResource{}
|
||||
resourcesByGVR := map[schema.GroupVersionResource]metav1.APIResource{}
|
||||
|
||||
for _, list := range resources {
|
||||
defaultGroupVersion, err := schema.ParseGroupVersion(list.GroupVersion)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get GroupVersion for: %+v", list)
|
||||
continue
|
||||
}
|
||||
for _, resource := range list.APIResources {
|
||||
if resource.Group == "" {
|
||||
resource.Group = defaultGroupVersion.Group
|
||||
}
|
||||
if resource.Version == "" {
|
||||
resource.Version = defaultGroupVersion.Version
|
||||
}
|
||||
gvr := defaultGroupVersion.WithResource(resource.Name)
|
||||
resourcesByGVR[gvr] = resource
|
||||
if shouldTestResource(gvr, resource) {
|
||||
gvrsToTest = append(gvrsToTest, gvr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(gvrsToTest, func(i, j int) bool {
|
||||
if gvrsToTest[i].Group < gvrsToTest[j].Group {
|
||||
return true
|
||||
}
|
||||
if gvrsToTest[i].Group > gvrsToTest[j].Group {
|
||||
return false
|
||||
}
|
||||
if gvrsToTest[i].Version < gvrsToTest[j].Version {
|
||||
return true
|
||||
}
|
||||
if gvrsToTest[i].Version > gvrsToTest[j].Version {
|
||||
return false
|
||||
}
|
||||
if gvrsToTest[i].Resource < gvrsToTest[j].Resource {
|
||||
return true
|
||||
}
|
||||
if gvrsToTest[i].Resource > gvrsToTest[j].Resource {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// map unqualified resource names to the fully qualified resource we will expect to be converted to
|
||||
// Note: this only works because there are no overlapping resource names in-process that are not co-located
|
||||
convertedResources := map[string]schema.GroupVersionResource{}
|
||||
// build the webhook rules enumerating the specific group/version/resources we want
|
||||
convertedV1beta1Rules := []admissionregistrationv1beta1.NamedRuleWithOperations{}
|
||||
convertedV1alpha1Rules := []admissionregistrationv1alpha1.NamedRuleWithOperations{}
|
||||
for _, gvr := range gvrsToTest {
|
||||
metaGVR := metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
|
||||
|
||||
convertedGVR, ok := convertedResources[gvr.Resource]
|
||||
if !ok {
|
||||
// this is the first time we've seen this resource
|
||||
// record the fully qualified resource we expect
|
||||
convertedGVR = gvr
|
||||
convertedResources[gvr.Resource] = gvr
|
||||
// add an admission rule indicating we can receive this version
|
||||
convertedV1beta1Rules = append(convertedV1beta1Rules, admissionregistrationv1beta1.NamedRuleWithOperations{
|
||||
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
|
||||
Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.OperationAll},
|
||||
Rule: admissionregistrationv1beta1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
|
||||
},
|
||||
})
|
||||
convertedV1alpha1Rules = append(convertedV1alpha1Rules, admissionregistrationv1alpha1.NamedRuleWithOperations{
|
||||
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
|
||||
Operations: []admissionregistrationv1alpha1.OperationType{admissionregistrationv1alpha1.OperationAll},
|
||||
Rule: admissionregistrationv1alpha1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// record the expected resource and kind
|
||||
holder.gvrToConvertedGVR[metaGVR] = metav1.GroupVersionResource{Group: convertedGVR.Group, Version: convertedGVR.Version, Resource: convertedGVR.Resource}
|
||||
holder.gvrToConvertedGVK[metaGVR] = schema.GroupVersionKind{Group: resourcesByGVR[convertedGVR].Group, Version: resourcesByGVR[convertedGVR].Version, Kind: resourcesByGVR[convertedGVR].Kind}
|
||||
}
|
||||
|
||||
if err := createV1alpha1ValidatingPolicyAndBinding(client, convertedV1alpha1Rules); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := createV1beta1ValidatingPolicyAndBinding(client, convertedV1beta1Rules); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Allow the policy & binding to establish
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
start := time.Now()
|
||||
count := 0
|
||||
|
||||
// Test admission on all resources, subresources, and verbs
|
||||
for _, gvr := range gvrsToTest {
|
||||
resource := resourcesByGVR[gvr]
|
||||
t.Run(gvr.Group+"."+gvr.Version+"."+strings.ReplaceAll(resource.Name, "/", "."), func(t *testing.T) {
|
||||
for _, verb := range []string{"create", "update", "patch", "connect", "delete", "deletecollection"} {
|
||||
if shouldTestResourceVerb(gvr, resource, verb) {
|
||||
t.Run(verb, func(t *testing.T) {
|
||||
count++
|
||||
holder.reset(t)
|
||||
testFunc := getTestFunc(gvr, verb)
|
||||
testFunc(&testContext{
|
||||
t: t,
|
||||
admissionHolder: holder,
|
||||
client: dynamicClient,
|
||||
clientset: client,
|
||||
verb: verb,
|
||||
gvr: gvr,
|
||||
resource: resource,
|
||||
resources: resourcesByGVR,
|
||||
})
|
||||
holder.verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if count >= 10 {
|
||||
duration := time.Since(start)
|
||||
perResourceDuration := time.Duration(int(duration) / count)
|
||||
if perResourceDuration >= 150*time.Millisecond {
|
||||
t.Errorf("expected resources to process in < 150ms, average was %v", perResourceDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Policy admission holder for test framework
|
||||
|
||||
type policyExpectationHolder struct {
|
||||
holder
|
||||
warningLock sync.Mutex
|
||||
warnings []string
|
||||
}
|
||||
|
||||
func (p *policyExpectationHolder) reset(t *testing.T) {
|
||||
p.warningLock.Lock()
|
||||
defer p.warningLock.Unlock()
|
||||
p.warnings = nil
|
||||
|
||||
p.holder.reset(t)
|
||||
|
||||
}
|
||||
func (p *policyExpectationHolder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) {
|
||||
p.holder.expect(gvr, gvk, optionsGVK, operation, name, namespace, object, oldObject, options)
|
||||
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
// Set up the recorded map with nil records for all combinations
|
||||
p.recorded = map[webhookOptions]*admissionRequest{}
|
||||
for _, phase := range []string{validation} {
|
||||
for _, converted := range []bool{true, false} {
|
||||
for _, version := range []string{"v1alpha1", "v1beta1"} {
|
||||
p.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *policyExpectationHolder) verify(t *testing.T) {
|
||||
p.warningLock.Lock()
|
||||
defer p.warningLock.Unlock()
|
||||
|
||||
// Process all detected warnings and record in the nested handler
|
||||
for _, w := range p.warnings {
|
||||
var currentRequest *admissionRequest
|
||||
var currentParams webhookOptions
|
||||
if idx := strings.Index(w, beginSentinel); idx >= 0 {
|
||||
|
||||
csvData := strings.ReplaceAll(w[idx+len(beginSentinel):], recordSeparator, "\n")
|
||||
|
||||
b := bytes.Buffer{}
|
||||
b.WriteString(csvData)
|
||||
reader := csv.NewReader(&b)
|
||||
csvRecords, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
mappedCSV := []map[string]string{}
|
||||
var header []string
|
||||
for line, record := range csvRecords {
|
||||
if line == 0 {
|
||||
header = record
|
||||
} else {
|
||||
line := map[string]string{}
|
||||
for i := 0; i < len(record); i++ {
|
||||
line[header[i]] = record[i]
|
||||
}
|
||||
mappedCSV = append(mappedCSV, line)
|
||||
}
|
||||
}
|
||||
|
||||
if len(mappedCSV) != 1 {
|
||||
t.Fatal("incorrect # CSV elements in parsed warning")
|
||||
return
|
||||
}
|
||||
|
||||
data := mappedCSV[0]
|
||||
currentRequest = &admissionRequest{
|
||||
Operation: data["operation"],
|
||||
Name: data["name"],
|
||||
Namespace: data["namespace"],
|
||||
Resource: metav1.GroupVersionResource{
|
||||
Group: data["resourceGroup"],
|
||||
Version: data["resourceVersion"],
|
||||
Resource: data["resourceResource"],
|
||||
},
|
||||
SubResource: data["subresource"],
|
||||
}
|
||||
currentParams = webhookOptions{
|
||||
version: data["paramsVersion"],
|
||||
phase: data["paramsPhase"],
|
||||
converted: data["paramsConvert"] == "true",
|
||||
}
|
||||
|
||||
if e, ok := data["objectExists"]; ok && e == "true" {
|
||||
currentRequest.Object.Object = &unstructured.Unstructured{}
|
||||
currentRequest.Object.Object.(*unstructured.Unstructured).SetAPIVersion(data["objectAPIVersion"])
|
||||
currentRequest.Object.Object.(*unstructured.Unstructured).SetKind(data["objectKind"])
|
||||
}
|
||||
|
||||
if e, ok := data["oldObjectExists"]; ok && e == "true" {
|
||||
currentRequest.OldObject.Object = &unstructured.Unstructured{}
|
||||
currentRequest.OldObject.Object.(*unstructured.Unstructured).SetAPIVersion(data["oldObjectAPIVersion"])
|
||||
currentRequest.OldObject.Object.(*unstructured.Unstructured).SetKind(data["oldObjectKind"])
|
||||
}
|
||||
|
||||
if e, ok := data["optionsExists"]; ok && e == "true" {
|
||||
currentRequest.Options.Object = &unstructured.Unstructured{}
|
||||
currentRequest.Options.Object.(*unstructured.Unstructured).SetAPIVersion(data["optionsAPIVersion"])
|
||||
currentRequest.Options.Object.(*unstructured.Unstructured).SetKind(data["optionsKind"])
|
||||
}
|
||||
|
||||
p.holder.record(currentParams.version, currentParams.phase, currentParams.converted, currentRequest)
|
||||
}
|
||||
}
|
||||
|
||||
p.holder.verify(t)
|
||||
}
|
||||
|
||||
func (p *policyExpectationHolder) HandleWarningHeader(code int, agent string, message string) {
|
||||
if code != 299 || len(message) == 0 {
|
||||
return
|
||||
}
|
||||
p.warningLock.Lock()
|
||||
defer p.warningLock.Unlock()
|
||||
p.warnings = append(p.warnings, message)
|
||||
}
|
||||
1095
test/integration/apiserver/cel/admission_test_util.go
Normal file
1095
test/integration/apiserver/cel/admission_test_util.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user