[PodSecurity] Add metrics test coverage

This commit is contained in:
Tim Allclair 2021-11-02 13:52:19 -07:00
parent c3398729e0
commit 81661d5a34
4 changed files with 217 additions and 171 deletions

View File

@ -228,7 +228,7 @@ func (a *Admission) Validate(ctx context.Context, attrs api.Attributes) *admissi
func (a *Admission) ValidateNamespace(ctx context.Context, attrs api.Attributes) *admissionv1.AdmissionResponse { func (a *Admission) ValidateNamespace(ctx context.Context, attrs api.Attributes) *admissionv1.AdmissionResponse {
// short-circuit on subresources // short-circuit on subresources
if attrs.GetSubresource() != "" { if attrs.GetSubresource() != "" {
return sharedAllowedResponse() return sharedAllowedResponse
} }
obj, err := attrs.GetObject() obj, err := attrs.GetObject()
if err != nil { if err != nil {
@ -249,7 +249,7 @@ func (a *Admission) ValidateNamespace(ctx context.Context, attrs api.Attributes)
if len(newErrs) > 0 { if len(newErrs) > 0 {
return invalidResponse(attrs, newErrs) return invalidResponse(attrs, newErrs)
} }
return sharedAllowedResponse() return sharedAllowedResponse
case admissionv1.Update: case admissionv1.Update:
// if update, check if policy labels changed // if update, check if policy labels changed
@ -276,24 +276,24 @@ func (a *Admission) ValidateNamespace(ctx context.Context, attrs api.Attributes)
// * if the new enforce is the same version and level was relaxed // * if the new enforce is the same version and level was relaxed
// * for exempt namespaces // * for exempt namespaces
if newPolicy.Enforce == oldPolicy.Enforce { if newPolicy.Enforce == oldPolicy.Enforce {
return sharedAllowedResponse() return sharedAllowedResponse
} }
if newPolicy.Enforce.Level == api.LevelPrivileged { if newPolicy.Enforce.Level == api.LevelPrivileged {
return sharedAllowedResponse() return sharedAllowedResponse
} }
if newPolicy.Enforce.Version == oldPolicy.Enforce.Version && if newPolicy.Enforce.Version == oldPolicy.Enforce.Version &&
api.CompareLevels(newPolicy.Enforce.Level, oldPolicy.Enforce.Level) < 1 { api.CompareLevels(newPolicy.Enforce.Level, oldPolicy.Enforce.Level) < 1 {
return sharedAllowedResponse() return sharedAllowedResponse
} }
if a.exemptNamespace(attrs.GetNamespace()) { if a.exemptNamespace(attrs.GetNamespace()) {
return sharedAllowedByNamespaceExemptionResponse() return sharedAllowedByNamespaceExemptionResponse
} }
response := allowedResponse() response := allowedResponse()
response.Warnings = a.EvaluatePodsInNamespace(ctx, namespace.Name, newPolicy.Enforce) response.Warnings = a.EvaluatePodsInNamespace(ctx, namespace.Name, newPolicy.Enforce)
return response return response
default: default:
return sharedAllowedResponse() return sharedAllowedResponse
} }
} }
@ -316,17 +316,17 @@ var ignoredPodSubresources = map[string]bool{
func (a *Admission) ValidatePod(ctx context.Context, attrs api.Attributes) *admissionv1.AdmissionResponse { func (a *Admission) ValidatePod(ctx context.Context, attrs api.Attributes) *admissionv1.AdmissionResponse {
// short-circuit on ignored subresources // short-circuit on ignored subresources
if ignoredPodSubresources[attrs.GetSubresource()] { if ignoredPodSubresources[attrs.GetSubresource()] {
return sharedAllowedResponse() return sharedAllowedResponse
} }
// short-circuit on exempt namespaces and users // short-circuit on exempt namespaces and users
if a.exemptNamespace(attrs.GetNamespace()) { if a.exemptNamespace(attrs.GetNamespace()) {
a.Metrics.RecordExemption(attrs) a.Metrics.RecordExemption(attrs)
return sharedAllowedByNamespaceExemptionResponse() return sharedAllowedByNamespaceExemptionResponse
} }
if a.exemptUser(attrs.GetUserName()) { if a.exemptUser(attrs.GetUserName()) {
a.Metrics.RecordExemption(attrs) a.Metrics.RecordExemption(attrs)
return sharedAllowedByUserExemptionResponse() return sharedAllowedByUserExemptionResponse
} }
// short-circuit on privileged enforce+audit+warn namespaces // short-circuit on privileged enforce+audit+warn namespaces
@ -339,7 +339,7 @@ func (a *Admission) ValidatePod(ctx context.Context, attrs api.Attributes) *admi
nsPolicy, nsPolicyErrs := a.PolicyToEvaluate(namespace.Labels) nsPolicy, nsPolicyErrs := a.PolicyToEvaluate(namespace.Labels)
if len(nsPolicyErrs) == 0 && nsPolicy.Enforce.Level == api.LevelPrivileged && nsPolicy.Warn.Level == api.LevelPrivileged && nsPolicy.Audit.Level == api.LevelPrivileged { if len(nsPolicyErrs) == 0 && nsPolicy.Enforce.Level == api.LevelPrivileged && nsPolicy.Warn.Level == api.LevelPrivileged && nsPolicy.Audit.Level == api.LevelPrivileged {
a.Metrics.RecordEvaluation(metrics.DecisionAllow, nsPolicy.Enforce, metrics.ModeEnforce, attrs) a.Metrics.RecordEvaluation(metrics.DecisionAllow, nsPolicy.Enforce, metrics.ModeEnforce, attrs)
return sharedAllowedResponse() return sharedAllowedPrivilegedResponse
} }
obj, err := attrs.GetObject() obj, err := attrs.GetObject()
@ -369,7 +369,7 @@ func (a *Admission) ValidatePod(ctx context.Context, attrs api.Attributes) *admi
} }
if !isSignificantPodUpdate(pod, oldPod) { if !isSignificantPodUpdate(pod, oldPod) {
// Nothing we care about changed, so always allow the update. // Nothing we care about changed, so always allow the update.
return sharedAllowedResponse() return sharedAllowedResponse
} }
} }
return a.EvaluatePod(ctx, nsPolicy, nsPolicyErrs.ToAggregate(), &pod.ObjectMeta, &pod.Spec, attrs, true) return a.EvaluatePod(ctx, nsPolicy, nsPolicyErrs.ToAggregate(), &pod.ObjectMeta, &pod.Spec, attrs, true)
@ -380,17 +380,17 @@ func (a *Admission) ValidatePod(ctx context.Context, attrs api.Attributes) *admi
func (a *Admission) ValidatePodController(ctx context.Context, attrs api.Attributes) *admissionv1.AdmissionResponse { func (a *Admission) ValidatePodController(ctx context.Context, attrs api.Attributes) *admissionv1.AdmissionResponse {
// short-circuit on subresources // short-circuit on subresources
if attrs.GetSubresource() != "" { if attrs.GetSubresource() != "" {
return sharedAllowedResponse() return sharedAllowedResponse
} }
// short-circuit on exempt namespaces and users // short-circuit on exempt namespaces and users
if a.exemptNamespace(attrs.GetNamespace()) { if a.exemptNamespace(attrs.GetNamespace()) {
a.Metrics.RecordExemption(attrs) a.Metrics.RecordExemption(attrs)
return sharedAllowedByNamespaceExemptionResponse() return sharedAllowedByNamespaceExemptionResponse
} }
if a.exemptUser(attrs.GetUserName()) { if a.exemptUser(attrs.GetUserName()) {
a.Metrics.RecordExemption(attrs) a.Metrics.RecordExemption(attrs)
return sharedAllowedByUserExemptionResponse() return sharedAllowedByUserExemptionResponse
} }
// short-circuit on privileged audit+warn namespaces // short-circuit on privileged audit+warn namespaces
@ -406,7 +406,7 @@ func (a *Admission) ValidatePodController(ctx context.Context, attrs api.Attribu
} }
nsPolicy, nsPolicyErrs := a.PolicyToEvaluate(namespace.Labels) nsPolicy, nsPolicyErrs := a.PolicyToEvaluate(namespace.Labels)
if len(nsPolicyErrs) == 0 && nsPolicy.Warn.Level == api.LevelPrivileged && nsPolicy.Audit.Level == api.LevelPrivileged { if len(nsPolicyErrs) == 0 && nsPolicy.Warn.Level == api.LevelPrivileged && nsPolicy.Audit.Level == api.LevelPrivileged {
return sharedAllowedResponse() return sharedAllowedResponse
} }
obj, err := attrs.GetObject() obj, err := attrs.GetObject()
@ -431,7 +431,7 @@ func (a *Admission) ValidatePodController(ctx context.Context, attrs api.Attribu
} }
if podMetadata == nil && podSpec == nil { if podMetadata == nil && podSpec == nil {
// if a controller with an optional pod spec does not contain a pod spec, skip validation // if a controller with an optional pod spec does not contain a pod spec, skip validation
return sharedAllowedResponse() return sharedAllowedResponse
} }
return a.EvaluatePod(ctx, nsPolicy, nsPolicyErrs.ToAggregate(), podMetadata, podSpec, attrs, false) return a.EvaluatePod(ctx, nsPolicy, nsPolicyErrs.ToAggregate(), podMetadata, podSpec, attrs, false)
} }
@ -443,7 +443,7 @@ func (a *Admission) EvaluatePod(ctx context.Context, nsPolicy api.Policy, nsPoli
// short-circuit on exempt runtimeclass // short-circuit on exempt runtimeclass
if a.exemptRuntimeClass(podSpec.RuntimeClassName) { if a.exemptRuntimeClass(podSpec.RuntimeClassName) {
a.Metrics.RecordExemption(attrs) a.Metrics.RecordExemption(attrs)
return sharedAllowedByRuntimeClassExemptionResponse() return sharedAllowedByRuntimeClassExemptionResponse
} }
auditAnnotations := map[string]string{} auditAnnotations := map[string]string{}

View File

@ -602,9 +602,11 @@ func TestValidatePodAndController(t *testing.T) {
exemptUser = "exempt-user" exemptUser = "exempt-user"
exemptRuntimeClass = "exempt-runtimeclass" exemptRuntimeClass = "exempt-runtimeclass"
podName = "test-pod"
) )
objMetadata := metav1.ObjectMeta{Name: "test-pod", Labels: map[string]string{"foo": "bar"}} objMetadata := metav1.ObjectMeta{Name: podName, Labels: map[string]string{"foo": "bar"}}
restrictedPod, err := test.GetMinimalValidPod(api.LevelRestricted, api.MajorMinorVersion(1, 23)) restrictedPod, err := test.GetMinimalValidPod(api.LevelRestricted, api.MajorMinorVersion(1, 23))
require.NoError(t, err) require.NoError(t, err)
@ -662,7 +664,7 @@ func TestValidatePodAndController(t *testing.T) {
baselineNs: makeNs(api.LevelBaseline, api.LevelBaseline, api.LevelBaseline), baselineNs: makeNs(api.LevelBaseline, api.LevelBaseline, api.LevelBaseline),
baselineWarnNs: makeNs("", api.LevelBaseline, ""), baselineWarnNs: makeNs("", api.LevelBaseline, ""),
baselineAuditNs: makeNs("", "", api.LevelBaseline), baselineAuditNs: makeNs("", "", api.LevelBaseline),
restrictedNs: makeNs(api.LevelRestricted, "", api.LevelRestricted), restrictedNs: makeNs(api.LevelRestricted, api.LevelRestricted, api.LevelRestricted),
invalidNs: makeNs("not-a-valid-level", "", ""), invalidNs: makeNs("not-a-valid-level", "", ""),
} }
@ -675,16 +677,6 @@ func TestValidatePodAndController(t *testing.T) {
evaluator, err := policy.NewEvaluator(policy.DefaultChecks()) evaluator, err := policy.NewEvaluator(policy.DefaultChecks())
assert.NoError(t, err) assert.NoError(t, err)
a := &Admission{
PodLister: &testPodLister{},
Evaluator: evaluator,
Configuration: config,
Metrics: &FakeRecorder{},
NamespaceGetter: nsGetter,
}
require.NoError(t, a.CompleteConfiguration(), "CompleteConfiguration()")
require.NoError(t, a.ValidateConfiguration(), "ValidateConfiguration()")
type testCase struct { type testCase struct {
desc string desc string
@ -707,11 +699,14 @@ func TestValidatePodAndController(t *testing.T) {
skipPod bool // Whether to skip the ValidatePod test case. skipPod bool // Whether to skip the ValidatePod test case.
skipDeployment bool // Whteher to skip the ValidatePodController test case. skipDeployment bool // Whteher to skip the ValidatePodController test case.
expectAllowed bool expectAllowed bool
expectReason metav1.StatusReason expectReason metav1.StatusReason
expectWarning bool expectExempt bool
expectEnforce bool // Whether to expect an enforcing evaluation (metric+annotation) expectError bool
expectedAuditAnnotationKeys []string
expectEnforce api.Level
expectWarning api.Level
expectAudit api.Level
} }
podCases := []testCase{ podCases := []testCase{
{ {
@ -722,85 +717,87 @@ func TestValidatePodAndController(t *testing.T) {
expectAllowed: true, expectAllowed: true,
}, },
{ {
desc: "exempt namespace", desc: "exempt namespace",
namespace: exemptNs, namespace: exemptNs,
pod: privilegedPod.DeepCopy(), pod: privilegedPod.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectedAuditAnnotationKeys: []string{"exempt"}, expectExempt: true,
}, },
{ {
desc: "exempt user", desc: "exempt user",
namespace: restrictedNs, namespace: restrictedNs,
username: exemptUser, username: exemptUser,
pod: privilegedPod.DeepCopy(), pod: privilegedPod.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectedAuditAnnotationKeys: []string{"exempt"}, expectExempt: true,
}, },
{ {
desc: "exempt runtimeClass", desc: "exempt runtimeClass",
namespace: restrictedNs, namespace: restrictedNs,
pod: exemptRCPod.DeepCopy(), pod: exemptRCPod.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectedAuditAnnotationKeys: []string{"exempt"}, expectExempt: true,
}, },
{ {
desc: "namespace not found", desc: "namespace not found",
namespace: "missing-ns", namespace: "missing-ns",
pod: restrictedPod.DeepCopy(), pod: restrictedPod.DeepCopy(),
expectAllowed: false, expectAllowed: false,
expectReason: metav1.StatusReasonInternalError, expectReason: metav1.StatusReasonInternalError,
expectedAuditAnnotationKeys: []string{"error"}, expectError: true,
}, },
{ {
desc: "short-circuit privileged:latest (implicit)", desc: "short-circuit privileged:latest (implicit)",
namespace: implicitNs, namespace: implicitNs,
pod: privilegedPod.DeepCopy(), pod: privilegedPod.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectEnforce: api.LevelPrivileged,
}, },
{ {
desc: "short-circuit privileged:latest (explicit)", desc: "short-circuit privileged:latest (explicit)",
namespace: privilegedNs, namespace: privilegedNs,
pod: privilegedPod.DeepCopy(), pod: privilegedPod.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectEnforce: api.LevelPrivileged,
}, },
{ {
desc: "failed decode", desc: "failed decode",
namespace: baselineNs, namespace: baselineNs,
objErr: fmt.Errorf("expected (failed decode)"), objErr: fmt.Errorf("expected (failed decode)"),
expectAllowed: false, expectAllowed: false,
expectReason: metav1.StatusReasonBadRequest, expectReason: metav1.StatusReasonBadRequest,
expectedAuditAnnotationKeys: []string{"error"}, expectError: true,
}, },
{ {
desc: "invalid object", desc: "invalid object",
namespace: baselineNs, namespace: baselineNs,
operation: admissionv1.Update, operation: admissionv1.Update,
obj: &corev1.Namespace{}, obj: &corev1.Namespace{},
expectAllowed: false, expectAllowed: false,
expectReason: metav1.StatusReasonBadRequest, expectReason: metav1.StatusReasonBadRequest,
expectedAuditAnnotationKeys: []string{"error"}, expectError: true,
}, },
{ {
desc: "failed decode old object", desc: "failed decode old object",
namespace: baselineNs, namespace: baselineNs,
operation: admissionv1.Update, operation: admissionv1.Update,
pod: restrictedPod.DeepCopy(), pod: restrictedPod.DeepCopy(),
oldObjErr: fmt.Errorf("expected (failed decode)"), oldObjErr: fmt.Errorf("expected (failed decode)"),
expectAllowed: false, expectAllowed: false,
expectReason: metav1.StatusReasonBadRequest, expectReason: metav1.StatusReasonBadRequest,
expectedAuditAnnotationKeys: []string{"error"}, expectError: true,
skipDeployment: true, // Updates aren't special cased for controller resources. skipDeployment: true, // Updates aren't special cased for controller resources.
}, },
{ {
desc: "invalid old object", desc: "invalid old object",
namespace: baselineNs, namespace: baselineNs,
operation: admissionv1.Update, operation: admissionv1.Update,
pod: restrictedPod.DeepCopy(), pod: restrictedPod.DeepCopy(),
oldObj: &corev1.Namespace{}, oldObj: &corev1.Namespace{},
expectAllowed: false, expectAllowed: false,
expectReason: metav1.StatusReasonBadRequest, expectReason: metav1.StatusReasonBadRequest,
expectedAuditAnnotationKeys: []string{"error"}, expectError: true,
skipDeployment: true, // Updates aren't special cased for controller resources. skipDeployment: true, // Updates aren't special cased for controller resources.
}, },
{ {
desc: "insignificant update", desc: "insignificant update",
@ -812,15 +809,16 @@ func TestValidatePodAndController(t *testing.T) {
skipDeployment: true, // Updates aren't special cased for controller resources. skipDeployment: true, // Updates aren't special cased for controller resources.
}, },
{ {
desc: "significant update denied", desc: "significant update denied",
namespace: restrictedNs, namespace: restrictedNs,
operation: admissionv1.Update, operation: admissionv1.Update,
pod: differentPrivilegedPod.DeepCopy(), pod: differentPrivilegedPod.DeepCopy(),
oldPod: privilegedPod.DeepCopy(), oldPod: privilegedPod.DeepCopy(),
expectAllowed: false, expectAllowed: false,
expectReason: metav1.StatusReasonForbidden, expectReason: metav1.StatusReasonForbidden,
expectEnforce: true, expectEnforce: api.LevelRestricted,
expectedAuditAnnotationKeys: []string{"audit-violations"}, expectWarning: api.LevelRestricted,
expectAudit: api.LevelRestricted,
}, },
{ {
desc: "significant update allowed", desc: "significant update allowed",
@ -829,55 +827,56 @@ func TestValidatePodAndController(t *testing.T) {
pod: differentRestrictedPod.DeepCopy(), pod: differentRestrictedPod.DeepCopy(),
oldPod: restrictedPod, oldPod: restrictedPod,
expectAllowed: true, expectAllowed: true,
expectEnforce: true, expectEnforce: api.LevelRestricted,
}, },
{ {
desc: "invalid namespace labels", desc: "invalid namespace labels",
namespace: invalidNs, namespace: invalidNs,
pod: baselinePod.DeepCopy(), pod: baselinePod.DeepCopy(),
expectAllowed: false, expectAllowed: false,
expectReason: metav1.StatusReasonForbidden, expectReason: metav1.StatusReasonForbidden,
expectEnforce: true, expectEnforce: api.LevelRestricted,
expectedAuditAnnotationKeys: []string{"error"}, expectError: true,
}, },
{ {
desc: "enforce deny", desc: "enforce deny",
namespace: restrictedNs, namespace: restrictedNs,
pod: privilegedPod.DeepCopy(), pod: privilegedPod.DeepCopy(),
expectAllowed: false, expectAllowed: false,
expectReason: metav1.StatusReasonForbidden, expectReason: metav1.StatusReasonForbidden,
expectEnforce: true, expectEnforce: api.LevelRestricted,
expectedAuditAnnotationKeys: []string{"audit-violations"}, expectWarning: api.LevelRestricted,
expectAudit: api.LevelRestricted,
}, },
{ {
desc: "enforce allow", desc: "enforce allow",
namespace: baselineNs, namespace: baselineNs,
pod: baselinePod.DeepCopy(), pod: baselinePod.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectEnforce: true, expectEnforce: api.LevelBaseline,
}, },
{ {
desc: "warn deny", desc: "warn deny",
namespace: baselineWarnNs, namespace: baselineWarnNs,
pod: privilegedPod.DeepCopy(), pod: privilegedPod.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectWarning: true, expectEnforce: api.LevelPrivileged,
expectEnforce: true, expectWarning: api.LevelBaseline,
}, },
{ {
desc: "audit deny", desc: "audit deny",
namespace: baselineAuditNs, namespace: baselineAuditNs,
pod: privilegedPod.DeepCopy(), pod: privilegedPod.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectEnforce: true, expectEnforce: api.LevelPrivileged,
expectedAuditAnnotationKeys: []string{"audit-violations"}, expectAudit: api.LevelBaseline,
}, },
{ {
desc: "no pod template", desc: "no pod template",
namespace: restrictedNs, namespace: restrictedNs,
obj: emptyDeployment.DeepCopy(), obj: emptyDeployment.DeepCopy(),
expectAllowed: true, expectAllowed: true,
expectWarning: false, // No pod template skips validation. expectWarning: "", // No pod template skips validation.
skipPod: true, skipPod: true,
}, },
} }
@ -904,15 +903,17 @@ func TestValidatePodAndController(t *testing.T) {
podTest.desc = "pod:" + tc.desc podTest.desc = "pod:" + tc.desc
podTest.resource = schema.GroupVersionResource{Version: "v1", Resource: "pods"} podTest.resource = schema.GroupVersionResource{Version: "v1", Resource: "pods"}
podTest.kind = schema.GroupVersionKind{Version: "v1", Kind: "Pod"} podTest.kind = schema.GroupVersionKind{Version: "v1", Kind: "Pod"}
if tc.expectEnforce { if !tc.expectAllowed {
podTest.expectedAuditAnnotationKeys = append(podTest.expectedAuditAnnotationKeys, "enforce-policy") podTest.expectWarning = "" // Warnings should only be returned when the request is allowed.
} }
deploymentTest := tc deploymentTest := tc
deploymentTest.desc = "deployment:" + tc.desc deploymentTest.desc = "deployment:" + tc.desc
deploymentTest.resource = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} deploymentTest.resource = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
deploymentTest.kind = schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"} deploymentTest.kind = schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
deploymentTest.expectAllowed = true // PodController validation is always non-enforcing. // PodController validation is always non-enforcing.
deploymentTest.expectAllowed = true
deploymentTest.expectEnforce = ""
deploymentTest.expectReason = "" deploymentTest.expectReason = ""
if tc.pod != nil { if tc.pod != nil {
@ -961,8 +962,21 @@ func TestValidatePodAndController(t *testing.T) {
attrs.Username = tc.username attrs.Username = tc.username
} }
recorder := &FakeRecorder{}
a := &Admission{
PodLister: &testPodLister{},
Evaluator: evaluator,
Configuration: config,
Metrics: recorder,
NamespaceGetter: nsGetter,
}
require.NoError(t, a.CompleteConfiguration(), "CompleteConfiguration()")
require.NoError(t, a.ValidateConfiguration(), "ValidateConfiguration()")
response := a.Validate(context.TODO(), attrs) response := a.Validate(context.TODO(), attrs)
var expectedEvaluations []MetricsRecord
var expectedAuditAnnotationKeys []string
if tc.expectAllowed { if tc.expectAllowed {
assert.True(t, response.Allowed, "Allowed") assert.True(t, response.Allowed, "Allowed")
assert.Nil(t, response.Result) assert.Nil(t, response.Result)
@ -973,42 +987,72 @@ func TestValidatePodAndController(t *testing.T) {
} }
} }
if tc.expectWarning { if tc.expectWarning != "" {
assert.NotEmpty(t, response.Warnings, "Warnings") assert.NotEmpty(t, response.Warnings, "Warnings")
} else { } else {
assert.Empty(t, response.Warnings, "Warnings") assert.Empty(t, response.Warnings, "Warnings")
} }
assert.Len(t, response.AuditAnnotations, len(tc.expectedAuditAnnotationKeys), "AuditAnnotations") if tc.expectEnforce != "" {
for _, key := range tc.expectedAuditAnnotationKeys { expectedAuditAnnotationKeys = append(expectedAuditAnnotationKeys, "enforce-policy")
record := MetricsRecord{podName, metrics.DecisionAllow, tc.expectEnforce, metrics.ModeEnforce}
if !tc.expectAllowed {
record.EvalDecision = metrics.DecisionDeny
}
expectedEvaluations = append(expectedEvaluations, record)
}
if tc.expectWarning != "" {
expectedEvaluations = append(expectedEvaluations, MetricsRecord{podName, metrics.DecisionDeny, tc.expectWarning, metrics.ModeWarn})
}
if tc.expectAudit != "" {
expectedEvaluations = append(expectedEvaluations, MetricsRecord{podName, metrics.DecisionDeny, tc.expectAudit, metrics.ModeAudit})
expectedAuditAnnotationKeys = append(expectedAuditAnnotationKeys, "audit-violations")
}
if tc.expectError {
expectedAuditAnnotationKeys = append(expectedAuditAnnotationKeys, "error")
assert.ElementsMatch(t, []MetricsRecord{{ObjectName: podName}}, recorder.errors, "expected RecordError() calls")
} else {
assert.Empty(t, recorder.errors, "expected RecordError() calls")
}
if tc.expectExempt {
expectedAuditAnnotationKeys = append(expectedAuditAnnotationKeys, "exempt")
assert.ElementsMatch(t, []MetricsRecord{{ObjectName: podName}}, recorder.exemptions, "expected RecordExemption() calls")
} else {
assert.Empty(t, recorder.exemptions, "expected RecordExemption() calls")
}
assert.Len(t, response.AuditAnnotations, len(expectedAuditAnnotationKeys), "AuditAnnotations")
for _, key := range expectedAuditAnnotationKeys {
assert.Contains(t, response.AuditAnnotations, key, "AuditAnnotations") assert.Contains(t, response.AuditAnnotations, key, "AuditAnnotations")
} }
assert.ElementsMatch(t, expectedEvaluations, recorder.evaluations, "expected RecordEvaluation() calls")
}) })
} }
} }
type FakeRecorder struct { type FakeRecorder struct {
evaluations []EvaluationRecord evaluations []MetricsRecord
exemptions []MetricsRecord
errors []MetricsRecord
} }
type EvaluationRecord struct { type MetricsRecord struct {
ObjectName string ObjectName string
Decision metrics.Decision EvalDecision metrics.Decision
Policy api.LevelVersion EvalPolicy api.Level
Mode metrics.Mode EvalMode metrics.Mode
} }
func (r *FakeRecorder) RecordEvaluation(decision metrics.Decision, policy api.LevelVersion, evalMode metrics.Mode, attrs api.Attributes) { func (r *FakeRecorder) RecordEvaluation(decision metrics.Decision, policy api.LevelVersion, evalMode metrics.Mode, attrs api.Attributes) {
r.evaluations = append(r.evaluations, EvaluationRecord{attrs.GetName(), decision, policy, evalMode}) r.evaluations = append(r.evaluations, MetricsRecord{attrs.GetName(), decision, policy.Level, evalMode})
} }
func (r *FakeRecorder) RecordExemption(api.Attributes) {} func (r *FakeRecorder) RecordExemption(attrs api.Attributes) {
func (r *FakeRecorder) RecordError(bool, api.Attributes) {} r.exemptions = append(r.exemptions, MetricsRecord{ObjectName: attrs.GetName()})
}
// ExpectEvaluation asserts that the evaluation was recorded, and clears the record. func (r *FakeRecorder) RecordError(_ bool, attrs api.Attributes) {
func (r *FakeRecorder) ExpectEvaluations(t *testing.T, expected []EvaluationRecord) { r.errors = append(r.errors, MetricsRecord{ObjectName: attrs.GetName()})
t.Helper()
assert.ElementsMatch(t, expected, r.evaluations)
} }
func TestPrioritizePods(t *testing.T) { func TestPrioritizePods(t *testing.T) {

View File

@ -21,15 +21,30 @@ import (
"os" "os"
"reflect" "reflect"
"testing" "testing"
admissionv1 "k8s.io/api/admission/v1"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
sharedCopy := sharedAllowedResponse().DeepCopy() sharedResponses := map[string]*admissionv1.AdmissionResponse{
"sharedAllowedResponse": sharedAllowedResponse,
"sharedAllowedPrivilegedResponse": sharedAllowedPrivilegedResponse,
"sharedAllowedByUserExemptionResponse": sharedAllowedByUserExemptionResponse,
"sharedAllowedByNamespaceExemptionResponse": sharedAllowedByNamespaceExemptionResponse,
"sharedAllowedByRuntimeClassExemptionResponse": sharedAllowedByRuntimeClassExemptionResponse,
}
sharedResponseCopies := map[string]*admissionv1.AdmissionResponse{}
for name, response := range sharedResponses {
sharedResponseCopies[name] = response.DeepCopy()
}
rc := m.Run() rc := m.Run()
if !reflect.DeepEqual(sharedCopy, sharedAllowedResponse()) { for name := range sharedResponses {
fmt.Println("sharedAllowedReponse mutated") if !reflect.DeepEqual(sharedResponseCopies[name], sharedResponses[name]) {
rc = 1 fmt.Fprintf(os.Stderr, "%s mutated\n", name)
rc = 1
}
} }
os.Exit(rc) os.Exit(rc)

View File

@ -27,26 +27,20 @@ import (
) )
var ( var (
_sharedAllowedResponse = allowedResponse() sharedAllowedResponse = allowedResponse()
_sharedAllowedByUserExemptionResponse = allowedByExemptResponse("user") sharedAllowedPrivilegedResponse = allowedResponse()
_sharedAllowedByNamespaceExemptionResponse = allowedByExemptResponse("namespace") sharedAllowedByUserExemptionResponse = allowedResponse()
_sharedAllowedByRuntimeClassExemptionResponse = allowedByExemptResponse("runtimeClass") sharedAllowedByNamespaceExemptionResponse = allowedResponse()
sharedAllowedByRuntimeClassExemptionResponse = allowedResponse()
) )
func sharedAllowedResponse() *admissionv1.AdmissionResponse { func init() {
return _sharedAllowedResponse sharedAllowedPrivilegedResponse.AuditAnnotations = map[string]string{
} api.EnforcedPolicyAnnotationKey: api.LevelVersion{Level: api.LevelPrivileged, Version: api.LatestVersion()}.String(),
}
func sharedAllowedByUserExemptionResponse() *admissionv1.AdmissionResponse { sharedAllowedByUserExemptionResponse.AuditAnnotations = map[string]string{api.ExemptionReasonAnnotationKey: "user"}
return _sharedAllowedByUserExemptionResponse sharedAllowedByNamespaceExemptionResponse.AuditAnnotations = map[string]string{api.ExemptionReasonAnnotationKey: "namespace"}
} sharedAllowedByRuntimeClassExemptionResponse.AuditAnnotations = map[string]string{api.ExemptionReasonAnnotationKey: "runtimeClass"}
func sharedAllowedByNamespaceExemptionResponse() *admissionv1.AdmissionResponse {
return _sharedAllowedByNamespaceExemptionResponse
}
func sharedAllowedByRuntimeClassExemptionResponse() *admissionv1.AdmissionResponse {
return _sharedAllowedByRuntimeClassExemptionResponse
} }
// allowedResponse is the response used when the admission decision is allow. // allowedResponse is the response used when the admission decision is allow.
@ -54,13 +48,6 @@ func allowedResponse() *admissionv1.AdmissionResponse {
return &admissionv1.AdmissionResponse{Allowed: true} return &admissionv1.AdmissionResponse{Allowed: true}
} }
func allowedByExemptResponse(exemptionReason string) *admissionv1.AdmissionResponse {
return &admissionv1.AdmissionResponse{
Allowed: true,
AuditAnnotations: map[string]string{api.ExemptionReasonAnnotationKey: exemptionReason},
}
}
// forbiddenResponse is the response used when the admission decision is deny for policy violations. // forbiddenResponse is the response used when the admission decision is deny for policy violations.
func forbiddenResponse(attrs api.Attributes, err error) *admissionv1.AdmissionResponse { func forbiddenResponse(attrs api.Attributes, err error) *admissionv1.AdmissionResponse {
return &admissionv1.AdmissionResponse{ return &admissionv1.AdmissionResponse{