diff --git a/staging/src/k8s.io/pod-security-admission/test/doc.go b/staging/src/k8s.io/pod-security-admission/test/doc.go new file mode 100644 index 00000000000..8948f786637 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2021 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 test contains tests for PodSecurity admission +package test // import "k8s.io/pod-security-admission/test" diff --git a/staging/src/k8s.io/pod-security-admission/test/fixtures.go b/staging/src/k8s.io/pod-security-admission/test/fixtures.go new file mode 100644 index 00000000000..2d32d4324a6 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/fixtures.go @@ -0,0 +1,193 @@ +/* +Copyright 2021 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 test + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/pod-security-admission/api" + "k8s.io/pod-security-admission/policy" + "k8s.io/utils/pointer" +) + +// minimalValidPods holds minimal valid pods per-level per-version. +// To get a valid pod for a particular level/version, use getMinimalValidPod(). +var minimalValidPods = map[api.Level]map[api.Version]*corev1.Pod{} + +func init() { + minimalValidPods[api.LevelBaseline] = map[api.Version]*corev1.Pod{} + minimalValidPods[api.LevelRestricted] = map[api.Version]*corev1.Pod{} + + // Define minimal valid baseline pod. + // This must remain valid for all versions. + baseline_1_0 := &corev1.Pod{Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{Name: "initcontainer1", Image: "k8s.gcr.io/pause"}}, + Containers: []corev1.Container{{Name: "container1", Image: "k8s.gcr.io/pause"}}}} + minimalValidPods[api.LevelBaseline][api.MajorMinorVersion(1, 0)] = baseline_1_0 + + // + // Define minimal valid restricted pods. + // + + // 1.0+: baseline + runAsNonRoot=true + restricted_1_0 := tweak(baseline_1_0, func(p *corev1.Pod) { + p.Spec.SecurityContext = &corev1.PodSecurityContext{RunAsNonRoot: pointer.BoolPtr(true)} + }) + minimalValidPods[api.LevelRestricted][api.MajorMinorVersion(1, 0)] = restricted_1_0 + + // 1.8+: runAsNonRoot=true + restricted_1_8 := tweak(restricted_1_0, func(p *corev1.Pod) { + p.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{AllowPrivilegeEscalation: pointer.BoolPtr(false)} + p.Spec.InitContainers[0].SecurityContext = &corev1.SecurityContext{AllowPrivilegeEscalation: pointer.BoolPtr(false)} + }) + minimalValidPods[api.LevelRestricted][api.MajorMinorVersion(1, 8)] = restricted_1_8 +} + +// getValidPod returns a minimal valid pod for the specified level and version. +func getMinimalValidPod(level api.Level, version api.Version) (*corev1.Pod, error) { + originalVersion := version + for { + pod, exists := minimalValidPods[level][version] + if exists { + return pod.DeepCopy(), nil + } + if version.Minor() <= 0 { + return nil, fmt.Errorf("no valid pod fixture found in specified or older versions for %s/%s", level, originalVersion.String()) + } + version = api.MajorMinorVersion(version.Major(), version.Minor()-1) + } +} + +// fixtureGenerators holds fixture generators per-level per-version. +// To add generators, use registerFixtureGenerator(). +// To get fixtures for a particular level/version, use getFixtures(). +var fixtureGenerators = map[fixtureKey]fixtureGenerator{} + +// fixtureKey is a tuple of version/level/check name +type fixtureKey struct { + version api.Version + level api.Level + check string +} + +// fixtureGenerator holds generators for valid and invalid fixtures. +type fixtureGenerator struct { + // expectErrorSubstring is a substring to expect in the error message for failed pods. + // if empty, the check ID is used. + expectErrorSubstring string + // generatePass transforms a minimum valid pod into one or more valid pods. + // pods do not need to populate metadata.name. + generatePass func(*corev1.Pod) []*corev1.Pod + // generateFail transforms a minimum valid pod into one or more invalid pods. + // pods do not need to populate metadata.name. + generateFail func(*corev1.Pod) []*corev1.Pod +} + +// fixtureData holds valid and invalid pod fixtures. +type fixtureData struct { + expectErrorSubstring string + + pass []*corev1.Pod + fail []*corev1.Pod +} + +// registerFixtureGenerator adds a generator for the given level/version/check. +// A generator registered for v1.x is used for v1.x+ if no generator is registered for the higher version. +func registerFixtureGenerator(key fixtureKey, generator fixtureGenerator) { + if err := checkKey(key); err != nil { + panic(err) + } + if _, exists := fixtureGenerators[key]; exists { + panic(fmt.Errorf("fixture generator already registered for key %#v", key)) + } + if generator.generatePass == nil || generator.generateFail == nil { + panic(fmt.Errorf("adding %#v: must specify generatePass/generateFail", key)) + } + fixtureGenerators[key] = generator + + if key.level == api.LevelBaseline { + // also register to restricted + restrictedKey := key + restrictedKey.level = api.LevelRestricted + if _, exists := fixtureGenerators[restrictedKey]; exists { + panic(fmt.Errorf("fixture generator already registered for restricted version of key %#v", key)) + } + fixtureGenerators[restrictedKey] = generator + } +} + +// getFixtures returns the fixture data for the specified level/version/check. +// Fixtures are generated by applying the registered generator to the minimal valid pod for that level/version. +// If no fixture generator exists for the given version, previous generators are checked back to 1.0. +func getFixtures(key fixtureKey) (fixtureData, error) { + if err := checkKey(key); err != nil { + return fixtureData{}, err + } + + validPodForLevel, err := getMinimalValidPod(key.level, key.version) + if err != nil { + return fixtureData{}, err + } + + for { + if generator, exists := fixtureGenerators[key]; exists { + data := fixtureData{ + expectErrorSubstring: generator.expectErrorSubstring, + + pass: generator.generatePass(validPodForLevel.DeepCopy()), + fail: generator.generateFail(validPodForLevel.DeepCopy()), + } + if len(data.expectErrorSubstring) == 0 { + data.expectErrorSubstring = key.check + } + if len(data.pass) == 0 || len(data.fail) == 0 { + return fixtureData{}, fmt.Errorf("generatePass/generateFail for %#v must return at least one pod each", key) + } + return data, nil + } + if key.version.Minor() == 0 { + return fixtureData{}, fmt.Errorf("no fixture generator found in specified or older versions for %#v", key) + } + // check the next older version + key.version = api.MajorMinorVersion(key.version.Major(), key.version.Minor()-1) + } +} + +// checkKey ensures the fixture key has a valid level, version, and check. +func checkKey(key fixtureKey) error { + if key.level != api.LevelBaseline && key.level != api.LevelRestricted { + return fmt.Errorf("invalid key, level must be baseline or restricted: %#v", key) + } + if key.version.Latest() || key.version.Major() != 1 || key.version.Minor() < 0 { + return fmt.Errorf("invalid key, version must be 1.0+: %#v", key) + } + if key.check == "" { + return fmt.Errorf("invalid key, check must not be empty") + } + found := false + for _, check := range policy.DefaultChecks() { + if check.ID == key.check { + found = true + break + } + } + if !found { + return fmt.Errorf("invalid key %#v, check does not exist at version", key) + } + return nil +} diff --git a/staging/src/k8s.io/pod-security-admission/test/fixtures_test.go b/staging/src/k8s.io/pod-security-admission/test/fixtures_test.go new file mode 100644 index 00000000000..0fbf5eda3a6 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/fixtures_test.go @@ -0,0 +1,155 @@ +/* +Copyright 2021 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 test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/pod-security-admission/api" + "k8s.io/pod-security-admission/policy" + "sigs.k8s.io/yaml" +) + +const updateEnvVar = "UPDATE_POD_SECURITY_FIXTURE_DATA" + +// TestFixtures ensures fixtures are registered for every check, +// and that in-memory fixtures match serialized fixtures in testdata. +// When adding new versions or checks, serialized fixtures can be updated by running: +// +// UPDATE_POD_SECURITY_FIXTURE_DATA=true go test k8s.io/pod-security-admission/test +func TestFixtures(t *testing.T) { + expectedFiles := sets.NewString("testdata/README.md") + + defaultChecks := policy.DefaultChecks() + + for _, level := range []api.Level{api.LevelBaseline, api.LevelRestricted} { + // TODO: derive from registered levels + for version := 0; version <= 22; version++ { + passDir := filepath.Join("testdata", string(level), fmt.Sprintf("v1.%d", version), "pass") + failDir := filepath.Join("testdata", string(level), fmt.Sprintf("v1.%d", version), "fail") + + // render the minimal valid pod fixture + validPod, err := getMinimalValidPod(level, api.MajorMinorVersion(1, version)) + if err != nil { + t.Fatal(err) + } + expectedFiles.Insert(testFixtureFile(t, passDir, "base", validPod)) + + // render check-specific fixtures + checkIDs, err := checksForLevelAndVersion(defaultChecks, level, api.MajorMinorVersion(1, version)) + if err != nil { + t.Fatal(err) + } + if len(checkIDs) == 0 { + t.Fatal(fmt.Errorf("no checks registered for %s/1.%d", level, version)) + } + for _, checkID := range checkIDs { + checkData, err := getFixtures(fixtureKey{level: level, version: api.MajorMinorVersion(1, version), check: checkID}) + if err != nil { + t.Fatal(err) + } + + for i, pod := range checkData.pass { + expectedFiles.Insert(testFixtureFile(t, passDir, fmt.Sprintf("%s%d", strings.ToLower(checkID), i), pod)) + } + for i, pod := range checkData.fail { + expectedFiles.Insert(testFixtureFile(t, failDir, fmt.Sprintf("%s%d", strings.ToLower(checkID), i), pod)) + } + } + } + } + + actualFileList := []string{} + err := filepath.Walk("testdata", func(path string, f os.FileInfo, err error) error { + if !f.IsDir() { + actualFileList = append(actualFileList, path) + } + return nil + }) + if err != nil { + t.Fatal(err) + } + actualFiles := sets.NewString(actualFileList...) + if missingFiles := expectedFiles.Difference(actualFiles); len(missingFiles) > 0 { + t.Errorf("unexpected missing fixtures:\n%s", strings.Join(missingFiles.List(), "\n")) + } + if extraFiles := actualFiles.Difference(expectedFiles); len(extraFiles) > 0 { + t.Errorf("unexpected extra fixtures:\n%s", strings.Join(extraFiles.List(), "\n")) + if os.Getenv(updateEnvVar) == "true" { + for extra := range extraFiles { + os.Remove(extra) + } + t.Logf("Removed extra fixture files") + t.Logf("Verify the diff, commit changes, and rerun the tests") + } else { + t.Logf("If the files are expected to be removed, re-run with %s=true to drop extra fixture files", updateEnvVar) + } + } + +} + +func testFixtureFile(t *testing.T, dir, name string, pod *corev1.Pod) string { + filename := filepath.Join(dir, name+".yaml") + pod = pod.DeepCopy() + pod.Name = name + + expectedYAML, _ := ioutil.ReadFile(filename) + + jsonData, err := runtime.Encode(scheme.Codecs.LegacyCodec(corev1.SchemeGroupVersion), pod) + if err != nil { + t.Fatal(err) + } + yamlData, err := yaml.JSONToYAML(jsonData) + if err != nil { + t.Fatal(err) + } + + // clean up noise in fixtures + yamlData = []byte(strings.ReplaceAll(string(yamlData), " creationTimestamp: null\n", "")) + yamlData = []byte(strings.ReplaceAll(string(yamlData), " resources: {}\n", "")) + yamlData = []byte(strings.ReplaceAll(string(yamlData), "status: {}\n", "")) + + if string(yamlData) != string(expectedYAML) { + t.Errorf("fixture data does not match the test fixture in %s", filename) + + if os.Getenv(updateEnvVar) == "true" { + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filename, []byte(yamlData), os.FileMode(0755)); err == nil { + t.Logf("Updated data in %s", filename) + t.Logf("Verify the diff, commit changes, and rerun the tests") + } else { + t.Logf("Could not update data in %s: %v", filename, err) + } + } else { + t.Logf("Diff between generated data and fixture data in %s:\n-------------\n%s", filename, diff.StringDiff(string(yamlData), string(expectedYAML))) + t.Logf("If the change is expected, re-run with %s=true to update the fixtures", updateEnvVar) + } + } + return filename +} diff --git a/staging/src/k8s.io/pod-security-admission/test/helpers.go b/staging/src/k8s.io/pod-security-admission/test/helpers.go new file mode 100644 index 00000000000..81250e0f56b --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/helpers.go @@ -0,0 +1,67 @@ +/* +Copyright 2021 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 test + +import ( + corev1 "k8s.io/api/core/v1" +) + +// tweak makes a copy of in, passes it to f(), and returns the result. +// the input is not modified. +func tweak(in *corev1.Pod, f func(copy *corev1.Pod)) *corev1.Pod { + out := in.DeepCopy() + f(out) + return out +} + +// ensureSecurityContext ensures the pod and all initContainers and containers have a non-nil security context. +func ensureSecurityContext(p *corev1.Pod) *corev1.Pod { + p = p.DeepCopy() + if p.Spec.SecurityContext == nil { + p.Spec.SecurityContext = &corev1.PodSecurityContext{} + } + for i := range p.Spec.Containers { + if p.Spec.Containers[i].SecurityContext == nil { + p.Spec.Containers[i].SecurityContext = &corev1.SecurityContext{} + } + } + for i := range p.Spec.InitContainers { + if p.Spec.InitContainers[i].SecurityContext == nil { + p.Spec.InitContainers[i].SecurityContext = &corev1.SecurityContext{} + } + } + return p +} + +// ensureSELinuxOptions ensures the pod and all initContainers and containers have a non-nil seLinuxOptions. +func ensureSELinuxOptions(p *corev1.Pod) *corev1.Pod { + p = ensureSecurityContext(p) + if p.Spec.SecurityContext.SELinuxOptions == nil { + p.Spec.SecurityContext.SELinuxOptions = &corev1.SELinuxOptions{} + } + for i := range p.Spec.Containers { + if p.Spec.Containers[i].SecurityContext.SELinuxOptions == nil { + p.Spec.Containers[i].SecurityContext.SELinuxOptions = &corev1.SELinuxOptions{} + } + } + for i := range p.Spec.InitContainers { + if p.Spec.InitContainers[i].SecurityContext.SELinuxOptions == nil { + p.Spec.InitContainers[i].SecurityContext.SELinuxOptions = &corev1.SELinuxOptions{} + } + } + return p +} diff --git a/staging/src/k8s.io/pod-security-admission/test/run.go b/staging/src/k8s.io/pod-security-admission/test/run.go new file mode 100644 index 00000000000..61e6d28e36a --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/run.go @@ -0,0 +1,300 @@ +/* +Copyright 2021 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 test + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/pod-security-admission/api" + "k8s.io/pod-security-admission/policy" +) + +// Options hold configuration for running integration tests against an existing server. +type Options struct { + // ClientConfig is a client configuration with sufficient permission to create, update, and delete + // namespaces, pods, and pod-template-containing objects. + // Required. + ClientConfig *rest.Config + + // CreateNamespace is an optional stub for creating a namespace with the given name and labels. + // Returning an error fails the test. + // If nil, DefaultCreateNamespace is used. + CreateNamespace func(client kubernetes.Interface, name string, labels map[string]string) (*corev1.Namespace, error) + + // These are the check ids/starting versions to exercise. + // If unset, policy.DefaultChecks() are used. + Checks []policy.Check + + // ExemptClient is an optional client interface to exercise behavior of an exempt client. + ExemptClient kubernetes.Interface + // ExemptNamespaces are optional namespaces not expected to have PodSecurity controls enforced. + ExemptNamespaces []string + // ExemptRuntimeClasses are optional runtimeclasses not expected to have PodSecurity controls enforced. + ExemptRuntimeClasses []string +} + +func toJSON(pod *corev1.Pod) string { + data, _ := json.Marshal(pod) + return string(data) +} + +// checksForLevelAndVersion returns the set of check IDs that apply when evaluating the given level and version. +// checks are assumed to be well-formed and valid to pass to policy.NewEvaluator(). +// level must be api.LevelRestricted or api.LevelBaseline +func checksForLevelAndVersion(checks []policy.Check, level api.Level, version api.Version) ([]string, error) { + retval := []string{} + for _, check := range checks { + if !version.Older(check.Versions[0].MinimumVersion) && (level == check.Level || level == api.LevelRestricted) { + retval = append(retval, check.ID) + } + } + return retval, nil +} + +// maxMinorVersionToTest returns the maximum minor version to exercise for a given set of checks. +// checks are assumed to be well-formed and valid to pass to policy.NewEvaluator(). +func maxMinorVersionToTest(checks []policy.Check) (int, error) { + // start with the release under development (1.22 at time of writing). + // this can be incremented to the current version whenever is convenient. + maxTestMinor := 22 + for _, check := range checks { + lastCheckVersion := check.Versions[len(check.Versions)-1].MinimumVersion + if lastCheckVersion.Major() != 1 { + return 0, fmt.Errorf("expected major version 1, got %d", lastCheckVersion.Major()) + } + if lastCheckVersion.Minor() > maxTestMinor { + maxTestMinor = lastCheckVersion.Minor() + } + } + return maxTestMinor, nil +} + +type testWarningHandler struct { + lock sync.Mutex + warnings []string +} + +func (t *testWarningHandler) HandleWarningHeader(code int, agent string, warning string) { + t.lock.Lock() + defer t.lock.Unlock() + t.warnings = append(t.warnings, warning) +} +func (t *testWarningHandler) FlushWarnings() []string { + t.lock.Lock() + defer t.lock.Unlock() + warnings := t.warnings + t.warnings = nil + return warnings +} + +// and ensures pod fixtures expected to pass and fail against that level/version work as expected. +func Run(t *testing.T, opts Options) { + warningHandler := &testWarningHandler{} + + configCopy := rest.CopyConfig(opts.ClientConfig) + configCopy.WarningHandler = warningHandler + client, err := kubernetes.NewForConfig(configCopy) + if err != nil { + t.Fatalf("error creating client: %v", err) + } + + if opts.CreateNamespace == nil { + opts.CreateNamespace = DefaultCreateNamespace + } + if len(opts.Checks) == 0 { + opts.Checks = policy.DefaultChecks() + } + _, err = policy.NewEvaluator(opts.Checks) + if err != nil { + t.Fatalf("invalid checks: %v", err) + } + maxMinor, err := maxMinorVersionToTest(opts.Checks) + if err != nil { + t.Fatalf("invalid checks: %v", err) + } + + for _, level := range []api.Level{api.LevelBaseline, api.LevelRestricted} { + for minor := 0; minor <= maxMinor; minor++ { + version := api.MajorMinorVersion(1, minor) + + // create test name + ns := fmt.Sprintf("podsecurity-%s-1-%d", level, minor) + + // create namespace + _, err := opts.CreateNamespace(client, ns, map[string]string{ + api.EnforceLevelLabel: string(level), + api.EnforceVersionLabel: fmt.Sprintf("v1.%d", minor), + api.WarnLevelLabel: string(level), + api.WarnVersionLabel: fmt.Sprintf("v1.%d", minor), + }) + if err != nil { + t.Errorf("failed creating namespace %s: %v", ns, err) + continue + } + t.Cleanup(func() { + client.CoreV1().Namespaces().Delete(context.Background(), ns, metav1.DeleteOptions{}) + }) + + // create service account (to allow pod to pass serviceaccount admission) + sa, err := client.CoreV1().ServiceAccounts(ns).Create( + context.Background(), + &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "default"}}, + metav1.CreateOptions{}, + ) + if err != nil && !apierrors.IsAlreadyExists(err) { + t.Errorf("failed creating serviceaccount %s: %v", ns, err) + continue + } + t.Cleanup(func() { + client.CoreV1().ServiceAccounts(ns).Delete(context.Background(), sa.Name, metav1.DeleteOptions{}) + }) + + // create pod + createPod := func(t *testing.T, i int, pod *corev1.Pod, expectSuccess bool, expectErrorSubstring string) { + t.Helper() + // avoid mutating original pod fixture + pod = pod.DeepCopy() + // assign pod name and serviceaccount + pod.Name = "test" + pod.Spec.ServiceAccountName = "default" + // dry-run create + _, err := client.CoreV1().Pods(ns).Create(context.Background(), pod, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}) + if !expectSuccess { + if err == nil { + t.Errorf("%d: expected error creating %s, got none", i, toJSON(pod)) + return + } + if strings.Contains(err.Error(), policy.UnknownForbiddenReason) { + t.Errorf("%d: unexpected unknown forbidden reason creating %s: %v", i, toJSON(pod), err) + return + } + if !strings.Contains(err.Error(), expectErrorSubstring) { + t.Errorf("%d: expected error with substring %q, got %v", i, expectErrorSubstring, err) + return + } + } + if expectSuccess && err != nil { + t.Errorf("%d: unexpected error creating %s: %v", i, toJSON(pod), err) + } + } + + // create controller + createController := func(t *testing.T, i int, pod *corev1.Pod, expectSuccess bool, expectErrorSubstring string) { + t.Helper() + // avoid mutating original pod fixture + pod = pod.DeepCopy() + if pod.Labels == nil { + pod.Labels = map[string]string{} + } + pod.Labels["test"] = "true" + + warningHandler.FlushWarnings() + // dry-run create + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"test": "true"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: pod.ObjectMeta, + Spec: pod.Spec, + }, + }, + } + _, err := client.AppsV1().Deployments(ns).Create(context.Background(), deployment, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}) + if err != nil { + t.Errorf("%d: unexpected error creating controller with %s: %v", i, toJSON(pod), err) + return + } + warningText := strings.Join(warningHandler.FlushWarnings(), "; ") + if !expectSuccess { + if len(warningText) == 0 { + t.Errorf("%d: expected warnings creating %s, got none", i, toJSON(pod)) + return + } + if strings.Contains(warningText, policy.UnknownForbiddenReason) { + t.Errorf("%d: unexpected unknown forbidden reason creating %s: %v", i, toJSON(pod), warningText) + return + } + if !strings.Contains(warningText, expectErrorSubstring) { + t.Errorf("%d: expected warning with substring %q, got %v", i, expectErrorSubstring, warningText) + return + } + } + if expectSuccess && len(warningText) > 0 { + t.Errorf("%d: unexpected warning creating %s: %v", i, toJSON(pod), warningText) + } + } + + minimalValidPod, err := getMinimalValidPod(level, version) + if err != nil { + t.Fatal(err) + } + t.Run(ns+"_pass_base", func(t *testing.T) { + createPod(t, 0, minimalValidPod.DeepCopy(), true, "") + createController(t, 0, minimalValidPod.DeepCopy(), true, "") + }) + + checkIDs, err := checksForLevelAndVersion(opts.Checks, level, version) + if err != nil { + t.Fatal(err) + } + if len(checkIDs) == 0 { + t.Fatal(fmt.Errorf("no checks registered for %s/1.%d", level, minor)) + } + for _, checkID := range checkIDs { + checkData, err := getFixtures(fixtureKey{level: level, version: version, check: checkID}) + if err != nil { + t.Fatal(err) + } + + t.Run(ns+"_pass_"+checkID, func(t *testing.T) { + for i, pod := range checkData.pass { + createPod(t, i, pod, true, "") + createController(t, i, pod, true, "") + } + }) + t.Run(ns+"_fail_"+checkID, func(t *testing.T) { + for i, pod := range checkData.fail { + createPod(t, i, pod, false, checkData.expectErrorSubstring) + createController(t, i, pod, false, checkData.expectErrorSubstring) + } + }) + } + } + } +} + +func DefaultCreateNamespace(client kubernetes.Interface, name string, labels map[string]string) (*corev1.Namespace, error) { + return client.CoreV1().Namespaces().Create( + context.Background(), + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: name, Labels: labels}, + }, + metav1.CreateOptions{}, + ) +} diff --git a/staging/src/k8s.io/pod-security-admission/test/testdata/README.md b/staging/src/k8s.io/pod-security-admission/test/testdata/README.md new file mode 100644 index 00000000000..0ab05559029 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/testdata/README.md @@ -0,0 +1 @@ +The fixtures in this folder are generated by TestFixtures. \ No newline at end of file