Merge pull request #115668 from jiahuif-forks/feature/validating-admission-policy/type-system
Type System for ValidatingAdmissionPolicy
This commit is contained in:
@@ -138,12 +138,13 @@ var (
|
||||
// admissionExemptResources lists objects which are exempt from admission validation/mutation,
|
||||
// only resources exempted from admission processing by API server should be listed here.
|
||||
admissionExemptResources = map[schema.GroupVersionResource]bool{
|
||||
gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies/status"): true,
|
||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"): true,
|
||||
}
|
||||
|
||||
parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{
|
||||
|
@@ -67,6 +67,8 @@ var resetFieldsStatusData = map[schema.GroupVersionResource]string{
|
||||
gvr("resource.k8s.io", "v1alpha1", "podschedulings"): `{"status": {"resourceClaims": [{"name": "my-claim", "unsuitableNodes": ["node2"]}]}}`, // Not really a conflict with status_test.go: Apply just stores both nodes. Conflict testing therefore gets disabled for podschedulings.
|
||||
gvr("resource.k8s.io", "v1alpha1", "resourceclaims"): `{"status": {"driverName": "other.example.com"}}`,
|
||||
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
|
||||
// standard for []metav1.Condition
|
||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
|
||||
}
|
||||
|
||||
// resetFieldsStatusDefault conflicts with statusDefault
|
||||
@@ -151,6 +153,7 @@ var resetFieldsSpecData = map[schema.GroupVersionResource]string{
|
||||
gvr("resource.k8s.io", "v1alpha1", "resourceclaims"): `{"spec": {"resourceClassName": "class2name"}}`, // ResourceClassName is immutable, but that doesn't matter for the test.
|
||||
gvr("resource.k8s.io", "v1alpha1", "resourceclaimtemplates"): `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
|
||||
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{}`,
|
||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
|
||||
}
|
||||
|
||||
// TestResetFields makes sure that fieldManager does not own fields reset by the storage strategy.
|
||||
|
@@ -57,6 +57,8 @@ var statusData = map[schema.GroupVersionResource]string{
|
||||
gvr("resource.k8s.io", "v1alpha1", "podschedulings"): `{"status": {"resourceClaims": [{"name": "my-claim", "unsuitableNodes": ["node1"]}]}}`,
|
||||
gvr("resource.k8s.io", "v1alpha1", "resourceclaims"): `{"status": {"driverName": "example.com"}}`,
|
||||
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
|
||||
// standard for []metav1.Condition
|
||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
|
||||
}
|
||||
|
||||
const statusDefault = `{"status": {"conditions": [{"type": "MyStatus", "status":"True"}]}}`
|
||||
|
@@ -751,10 +751,13 @@ func Test_PolicyExemption(t *testing.T) {
|
||||
}
|
||||
|
||||
// validate that operations to ValidatingAdmissionPolicy are exempt from an existing policy that catches all resources
|
||||
policyCopy := policy.DeepCopy()
|
||||
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(context.TODO(), policy.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ignoreFailurePolicy := admissionregistrationv1alpha1.Ignore
|
||||
policyCopy.Spec.FailurePolicy = &ignoreFailurePolicy
|
||||
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Update(context.TODO(), policyCopy, metav1.UpdateOptions{})
|
||||
policy.Spec.FailurePolicy = &ignoreFailurePolicy
|
||||
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Update(context.TODO(), policy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -866,9 +869,12 @@ func Test_ValidatingAdmissionPolicy_UpdateParamKind(t *testing.T) {
|
||||
APIVersion: "v1",
|
||||
Kind: "Secret",
|
||||
}
|
||||
policyCopy := policy.DeepCopy()
|
||||
policyCopy.Spec.ParamKind = paramKind
|
||||
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Update(context.TODO(), policyCopy, metav1.UpdateOptions{})
|
||||
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(context.TODO(), policy.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
policy.Spec.ParamKind = paramKind
|
||||
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Update(context.TODO(), policy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -2611,6 +2617,26 @@ 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
|
||||
@@ -2885,3 +2911,114 @@ rules:
|
||||
resources: ["configmaps"]
|
||||
`
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user