mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
PodSecurity: test: framework
This commit is contained in:
parent
1436d35779
commit
29f5ebf1fe
18
staging/src/k8s.io/pod-security-admission/test/doc.go
Normal file
18
staging/src/k8s.io/pod-security-admission/test/doc.go
Normal file
@ -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"
|
193
staging/src/k8s.io/pod-security-admission/test/fixtures.go
Normal file
193
staging/src/k8s.io/pod-security-admission/test/fixtures.go
Normal file
@ -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
|
||||
}
|
155
staging/src/k8s.io/pod-security-admission/test/fixtures_test.go
Normal file
155
staging/src/k8s.io/pod-security-admission/test/fixtures_test.go
Normal file
@ -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
|
||||
}
|
67
staging/src/k8s.io/pod-security-admission/test/helpers.go
Normal file
67
staging/src/k8s.io/pod-security-admission/test/helpers.go
Normal file
@ -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
|
||||
}
|
300
staging/src/k8s.io/pod-security-admission/test/run.go
Normal file
300
staging/src/k8s.io/pod-security-admission/test/run.go
Normal file
@ -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{},
|
||||
)
|
||||
}
|
1
staging/src/k8s.io/pod-security-admission/test/testdata/README.md
vendored
Normal file
1
staging/src/k8s.io/pod-security-admission/test/testdata/README.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
The fixtures in this folder are generated by TestFixtures.
|
Loading…
Reference in New Issue
Block a user