From 7e3ae1060f336a5577546aaf309c02ebb2957661 Mon Sep 17 00:00:00 2001 From: Anish Shah Date: Tue, 22 Oct 2024 23:11:29 -0700 Subject: [PATCH] integration: RBAC tests for /resize request The test confirms that the subject can successfully resize the Pod resources but not the entire pod. --- .../cel/validatingadmissionpolicy_test.go | 28 +---- test/integration/authutil/authutil.go | 27 +++++ test/integration/pods/pods_test.go | 113 +++++++++++++++++- 3 files changed, 137 insertions(+), 31 deletions(-) diff --git a/test/integration/apiserver/cel/validatingadmissionpolicy_test.go b/test/integration/apiserver/cel/validatingadmissionpolicy_test.go index bf618da56d3..72e2fac9422 100644 --- a/test/integration/apiserver/cel/validatingadmissionpolicy_test.go +++ b/test/integration/apiserver/cel/validatingadmissionpolicy_test.go @@ -2589,7 +2589,7 @@ func Test_ValidateSecondaryAuthorization(t *testing.T) { }, { name: "serviceaccount is authorized for custom verb on current resource", - extraAccountFn: serviceAccountClient("default", "extra-acct"), + extraAccountFn: authutil.ServiceAccountClient("default", "extra-acct"), extraAccountRbac: &rbacv1.PolicyRule{ Verbs: []string{"anthropomorphize"}, APIGroups: []string{""}, @@ -2985,8 +2985,6 @@ contexts: } } -type clientFn func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset - func secondaryAuthorizationUserClient(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset { clientConfig = rest.CopyConfig(clientConfig) clientConfig.Impersonate = rest.ImpersonationConfig{ @@ -3002,29 +3000,7 @@ func secondaryAuthorizationUserClient(t *testing.T, adminClient *clientset.Clien } func secondaryAuthorizationServiceAccountClient(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset { - return serviceAccountClient("default", "test-service-acct")(t, adminClient, clientConfig, rules) -} - -func serviceAccountClient(namespace, name string) clientFn { - return func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset { - clientConfig = rest.CopyConfig(clientConfig) - sa, err := adminClient.CoreV1().ServiceAccounts(namespace).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: name}}, metav1.CreateOptions{}) - if err != nil && !apierrors.IsAlreadyExists(err) { - t.Fatal(err) - } - uid := sa.UID - - clientConfig.Impersonate = rest.ImpersonationConfig{ - UserName: "system:serviceaccount:" + namespace + ":" + name, - UID: string(uid), - } - client := clientset.NewForConfigOrDie(clientConfig) - - for _, rule := range rules { - authutil.GrantServiceAccountAuthorization(t, context.TODO(), adminClient, name, namespace, rule) - } - return client - } + return authutil.ServiceAccountClient("default", "test-service-acct")(t, adminClient, clientConfig, rules) } func withWaitReadyConstraintAndExpression(policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { diff --git a/test/integration/authutil/authutil.go b/test/integration/authutil/authutil.go index 8373de2360f..6bd169e86a7 100644 --- a/test/integration/authutil/authutil.go +++ b/test/integration/authutil/authutil.go @@ -23,12 +23,15 @@ import ( "time" authorizationv1 "k8s.io/api/authorization/v1" + v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" clientset "k8s.io/client-go/kubernetes" authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" + "k8s.io/client-go/rest" ) // WaitForNamedAuthorizationUpdate checks if the given user can perform the named verb and action on the named resource. @@ -132,3 +135,27 @@ func grantAuthorization(t *testing.T, ctx context.Context, adminClient clientset true, ) } + +type clientFn func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset + +func ServiceAccountClient(namespace, name string) clientFn { + return func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset { + clientConfig = rest.CopyConfig(clientConfig) + sa, err := adminClient.CoreV1().ServiceAccounts(namespace).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: name}}, metav1.CreateOptions{}) + if err != nil && !apierrors.IsAlreadyExists(err) { + t.Fatal(err) + } + uid := sa.UID + + clientConfig.Impersonate = rest.ImpersonationConfig{ + UserName: "system:serviceaccount:" + namespace + ":" + name, + UID: string(uid), + } + client := clientset.NewForConfigOrDie(clientConfig) + + for _, rule := range rules { + GrantServiceAccountAuthorization(t, context.TODO(), adminClient, name, namespace, rule) + } + return client + } +} diff --git a/test/integration/pods/pods_test.go b/test/integration/pods/pods_test.go index 8b0aa1afdf6..0214c5f0ce0 100644 --- a/test/integration/pods/pods_test.go +++ b/test/integration/pods/pods_test.go @@ -23,16 +23,20 @@ import ( "testing" v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" utilfeature "k8s.io/apiserver/pkg/util/feature" clientset "k8s.io/client-go/kubernetes" typedv1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" featuregatetesting "k8s.io/component-base/featuregate/testing" kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + rbachelper "k8s.io/kubernetes/pkg/apis/rbac/v1" "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/test/integration" + "k8s.io/kubernetes/test/integration/authutil" "k8s.io/kubernetes/test/integration/framework" ) @@ -679,16 +683,114 @@ func TestPodUpdateEphemeralContainers(t *testing.T) { } } -func TestPodResize(t *testing.T) { - // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. +func TestPodResizeRBAC(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true) server := kubeapiservertesting.StartTestServerOrDie(t, nil, - append(framework.DefaultTestServerFlags(), "--feature-gates=InPlacePodVerticalScaling=true"), - framework.SharedEtcd()) + append(framework.DefaultTestServerFlags(), "--authorization-mode=RBAC"), framework.SharedEtcd()) + defer server.TearDownFn() + adminClient := clientset.NewForConfigOrDie(server.ClientConfig) + + ns := framework.CreateNamespaceOrDie(adminClient, "pod-resize", t) + defer framework.DeleteNamespaceOrDie(adminClient, ns, t) + + testPod := func(name string) *v1.Pod { + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "fake-name", + Image: "fakeimage", + }, + }, + }, + } + } + + testcases := []struct { + name string + serviceAccountFn func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset + serviceAccountRBAC rbacv1.PolicyRule + allowResize bool + allowUpdate bool + }{ + { + name: "pod-mutator", + serviceAccountFn: authutil.ServiceAccountClient(ns.Name, "pod-mutator"), + serviceAccountRBAC: rbachelper.NewRule("get", "update", "patch").Groups("").Resources("pods").RuleOrDie(), + allowResize: false, + allowUpdate: true, + }, + { + name: "pod-resizer", + serviceAccountFn: authutil.ServiceAccountClient(ns.Name, "pod-resizer"), + serviceAccountRBAC: rbachelper.NewRule("get", "update", "patch").Groups("").Resources("pods/resize").RuleOrDie(), + allowResize: true, + allowUpdate: false, + }, + } + + for i, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + // 1. Create a test pod. + pod := testPod(fmt.Sprintf("resize-%d", i)) + resp, err := adminClient.CoreV1().Pods(ns.Name).Create(context.TODO(), pod, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Unexpected error when creating pod: %v", err) + integration.DeletePodOrErrorf(t, adminClient, ns.Name, pod.Name) + } + + // 2. Create a service account and fetch its client. + saClient := tc.serviceAccountFn(t, adminClient, server.ClientConfig, []rbacv1.PolicyRule{tc.serviceAccountRBAC}) + + // 3. Update pod and check whether it should be allowed. + resp.Spec.Containers[0].Image = "updated-image" + if _, err := saClient.CoreV1().Pods(ns.Name).Update(context.TODO(), resp, metav1.UpdateOptions{}); err == nil && !tc.allowUpdate { + t.Fatalf("Unexpected allowed pod update") + integration.DeletePodOrErrorf(t, adminClient, ns.Name, pod.Name) + } else if err != nil && tc.allowUpdate { + t.Fatalf("Unexpected error when updating pod container resources: %v", err) + integration.DeletePodOrErrorf(t, adminClient, ns.Name, pod.Name) + } + + // 4. Resize pod container resource and check whether it should be allowed. + resp, err = adminClient.CoreV1().Pods(ns.Name).Get(context.TODO(), resp.Name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Unexpected error when fetching the pod: %v", err) + integration.DeletePodOrErrorf(t, adminClient, ns.Name, pod.Name) + } + resp.Spec.Containers[0].Resources = v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceEphemeralStorage: resource.MustParse("2Gi"), + }, + } + _, err = saClient.CoreV1().Pods(ns.Name).Resize(context.TODO(), resp.Name, resp, metav1.UpdateOptions{}) + if tc.allowResize && err != nil { + t.Fatalf("Unexpected pod resize failure: %v", err) + integration.DeletePodOrErrorf(t, adminClient, ns.Name, pod.Name) + } + if !tc.allowResize && err == nil { + t.Fatalf("Unexpected pod resize success") + integration.DeletePodOrErrorf(t, adminClient, ns.Name, pod.Name) + } + + // 5. Delete the test pod. + integration.DeletePodOrErrorf(t, adminClient, ns.Name, pod.Name) + }) + } +} + +func TestPodResize(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true) + // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. + server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd()) defer server.TearDownFn() client := clientset.NewForConfigOrDie(server.ClientConfig) - ns := framework.CreateNamespaceOrDie(client, "pod-create-ephemeral-containers", t) + ns := framework.CreateNamespaceOrDie(client, "pod-resize", t) defer framework.DeleteNamespaceOrDie(client, ns, t) testPod := func(name string) *v1.Pod { @@ -773,6 +875,7 @@ func TestPodResize(t *testing.T) { integration.DeletePodOrErrorf(t, client, ns.Name, pod.Name) } else if !strings.Contains(err.Error(), "spec: Forbidden: pod updates may not change fields other than") { t.Fatalf("Unexpected error when updating pod container resources: %v", err) + integration.DeletePodOrErrorf(t, client, ns.Name, pod.Name) } resp, err = client.CoreV1().Pods(ns.Name).Resize(context.TODO(), resp.Name, resp, metav1.UpdateOptions{})