Files
Oliver BählerandCopilot Autofix powered by AI 77d1810bb9 feat: add metadata enforcement (#1990)
* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: preserve ca-bundles injected from external providers

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add metadata enforcement

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add metadata enforcement

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: add resourcepoolclaim validation

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: add resourcepoolclaim validation

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: add resourcepoolclaim validation

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: add resourcepoolclaim validation

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-02 13:42:08 +02:00

1088 lines
24 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package validation
import (
"context"
"errors"
"strings"
"testing"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/cache"
"github.com/projectcapsule/capsule/pkg/api/meta"
apirules "github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/api/runtime"
"github.com/projectcapsule/capsule/pkg/ruleengine"
"github.com/projectcapsule/capsule/pkg/runtime/events"
)
func TestGenericRules(t *testing.T) {
t.Parallel()
got := GenericRules(nil)
h, ok := got.(*genericRules)
if !ok {
t.Fatalf("expected *genericRules, got %T", got)
}
if h.regexCache == nil {
t.Fatalf("expected regex cache")
}
if len(h.rules) != 1 {
t.Fatalf("expected one generic validator, got %d", len(h.rules))
}
if !h.managedMetadata.HasLabel(meta.TenantLabel) {
t.Fatalf("expected default managed metadata to include %q", meta.TenantLabel)
}
if len(h.objectSkipRules) == 0 {
t.Fatalf("expected default object skip rules")
}
}
func TestEvaluateGenericRules(t *testing.T) {
t.Parallel()
t.Run("nil object returns nil", func(t *testing.T) {
t.Parallel()
got, err := evaluateGenericRules[runtime.ExpressionMatch](
nil,
[]*apirules.NamespaceRuleEnforceBody{
{
Action: apirules.ActionTypeAllow,
},
},
genericRuleSet[runtime.ExpressionMatch]{},
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got != nil {
t.Fatalf("expected nil evaluation, got %#v", got)
}
})
t.Run("empty enforce bodies returns nil", func(t *testing.T) {
t.Parallel()
got, err := evaluateGenericRules[runtime.ExpressionMatch](
genericMetadataObject(nil, nil),
nil,
genericRuleSet[runtime.ExpressionMatch]{},
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got != nil {
t.Fatalf("expected nil evaluation, got %#v", got)
}
})
t.Run("delegates to ruleengine", func(t *testing.T) {
t.Parallel()
set := genericRuleSet[runtime.ExpressionMatch]{
Name: "test",
EventReason: events.ReasonForbiddenMetadata,
AllowedDescription: "Allowed test values",
Values: func(obj genericObject) []ruleengine.Value {
return []ruleengine.Value{
{
Value: obj.GetLabels()["env"],
Path: `metadata.labels["env"]`,
},
}
},
Rules: func(enforce *apirules.NamespaceRuleEnforceBody) []runtime.ExpressionMatch {
return []runtime.ExpressionMatch{
{
Exact: []string{"prod"},
},
}
},
Matches: func(match runtime.ExpressionMatch, value ruleengine.Value) (ruleengine.Match, error) {
return ruleengine.Match{
Matched: value.Value == "prod",
}, nil
},
RuleDescription: func(match runtime.ExpressionMatch) string {
return runtime.DescribeExpressionMatch(match)
},
}
got, err := evaluateGenericRules(
genericMetadataObject(map[string]string{"env": "prod"}, nil),
[]*apirules.NamespaceRuleEnforceBody{
{
Action: apirules.ActionTypeAllow,
},
},
set,
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got == nil {
t.Fatalf("expected evaluation")
}
if err := got.BlockingError(); err != nil {
t.Fatalf("expected no blocking error, got %v", err)
}
})
}
func TestValidateGenericRules(t *testing.T) {
t.Parallel()
baseGVK := schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "ConfigMap",
}
t.Run("nil object returns nil and does not call validators", func(t *testing.T) {
t.Parallel()
h := &genericRules{
rules: []genericRuleValidator{
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
t.Fatalf("validator must not be called")
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
err := h.validateGenericRules(
context.Background(),
admission.Request{},
nil,
baseGVK,
testTenant(),
testEventRecorder{},
nil,
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
t.Run("sets gvk before running validators", func(t *testing.T) {
t.Parallel()
obj := genericMetadataObject(nil, nil)
h := &genericRules{
rules: []genericRuleValidator{
func(obj genericObject, got schema.GroupVersionKind, _ []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
if got != baseGVK {
t.Fatalf("expected gvk %s, got %s", baseGVK.String(), got.String())
}
if obj.GetObjectKind().GroupVersionKind() != baseGVK {
t.Fatalf(
"expected object gvk %s, got %s",
baseGVK.String(),
obj.GetObjectKind().GroupVersionKind().String(),
)
}
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
err := h.validateGenericRules(
context.Background(),
admission.Request{},
obj,
baseGVK,
testTenant(),
testEventRecorder{},
nil,
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
t.Run("skips controller managed objects before validators", func(t *testing.T) {
t.Parallel()
called := false
skipRules := meta.DefaultObjectSkipRules()
if len(skipRules) == 0 {
t.Fatalf("expected default object skip rules")
}
labels := map[string]string{}
for key, value := range skipRules[0].Labels {
labels[key] = value
}
if len(labels) == 0 {
t.Fatalf("expected default object skip rule to contain labels")
}
h := &genericRules{
rules: []genericRuleValidator{
func(
genericObject,
schema.GroupVersionKind,
[]*apirules.NamespaceRuleEnforceBody,
) (*ruleengine.Evaluation, error) {
called = true
return nil, nil
},
},
regexCache: cache.NewRegexCache(),
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: skipRules,
}
obj := genericMetadataObject(labels, nil)
err := h.validateGenericRules(
context.Background(),
admission.Request{},
obj,
coreGVK("ConfigMap"),
&capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-a",
},
},
testEventRecorder{},
[]*apirules.NamespaceRuleEnforceBody{
{
Action: apirules.ActionTypeAllow,
},
},
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if called {
t.Fatalf("validator must not be called for skipped object")
}
})
t.Run("does not skip non matching controller managed label value", func(t *testing.T) {
t.Parallel()
called := false
h := &genericRules{
rules: []genericRuleValidator{
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
called = true
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
err := h.validateGenericRules(
context.Background(),
admission.Request{},
genericMetadataObject(map[string]string{
"managed-by": "human",
}, nil),
baseGVK,
testTenant(),
testEventRecorder{},
nil,
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !called {
t.Fatalf("expected validator to be called")
}
})
t.Run("runs validators in order", func(t *testing.T) {
t.Parallel()
calls := []string{}
h := &genericRules{
rules: []genericRuleValidator{
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
calls = append(calls, "first")
return nil, nil
},
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
calls = append(calls, "second")
return &ruleengine.Evaluation{}, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
err := h.validateGenericRules(
context.Background(),
admission.Request{},
genericMetadataObject(nil, nil),
baseGVK,
testTenant(),
testEventRecorder{},
nil,
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got, want := strings.Join(calls, ","), "first,second"; got != want {
t.Fatalf("expected calls %q, got %q", want, got)
}
})
t.Run("passes enforce bodies to validators", func(t *testing.T) {
t.Parallel()
enforceBodies := []*apirules.NamespaceRuleEnforceBody{
{
Action: apirules.ActionTypeAllow,
},
{
Action: apirules.ActionTypeDeny,
},
}
h := &genericRules{
rules: []genericRuleValidator{
func(_ genericObject, _ schema.GroupVersionKind, got []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
if len(got) != len(enforceBodies) {
t.Fatalf("expected %d enforce bodies, got %d", len(enforceBodies), len(got))
}
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
err := h.validateGenericRules(
context.Background(),
admission.Request{},
genericMetadataObject(nil, nil),
baseGVK,
testTenant(),
testEventRecorder{},
enforceBodies,
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
t.Run("propagates validator error and stops", func(t *testing.T) {
t.Parallel()
expected := errors.New("boom")
secondCalled := false
h := &genericRules{
rules: []genericRuleValidator{
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
return nil, expected
},
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
secondCalled = true
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
err := h.validateGenericRules(
context.Background(),
admission.Request{},
genericMetadataObject(nil, nil),
baseGVK,
testTenant(),
testEventRecorder{},
nil,
)
if !errors.Is(err, expected) {
t.Fatalf("expected %v, got %v", expected, err)
}
if secondCalled {
t.Fatalf("second validator must not be called")
}
})
t.Run("handles audit-only evaluation and continues", func(t *testing.T) {
t.Parallel()
calls := 0
h := &genericRules{
rules: []genericRuleValidator{
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
calls++
return &ruleengine.Evaluation{
Audits: []*ruleengine.Decision{
{
SetName: "metadata label",
EventReason: events.ReasonForbiddenMetadata,
Action: apirules.ActionTypeAudit,
Value: ruleengine.Value{
Value: "audit",
Path: `metadata.labels["audit"]`,
},
Message: "audit message",
},
},
}, nil
},
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
calls++
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
err := h.validateGenericRules(
context.Background(),
admission.Request{},
genericMetadataObject(nil, nil),
baseGVK,
testTenant(),
testEventRecorder{},
nil,
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if calls != 2 {
t.Fatalf("expected two validators to be called, got %d", calls)
}
})
t.Run("returns blocking evaluation error and stops", func(t *testing.T) {
t.Parallel()
secondCalled := false
h := &genericRules{
rules: []genericRuleValidator{
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
return &ruleengine.Evaluation{
Blocking: &ruleengine.Decision{
SetName: "metadata label",
EventReason: events.ReasonForbiddenMetadata,
Action: apirules.ActionTypeDeny,
Value: ruleengine.Value{
Value: "stage",
Path: `metadata.labels["env"]`,
},
Message: "blocked",
},
}, nil
},
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
secondCalled = true
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
err := h.validateGenericRules(
context.Background(),
admission.Request{},
genericMetadataObject(nil, nil),
baseGVK,
testTenant(),
testEventRecorder{},
nil,
)
if err == nil {
t.Fatalf("expected blocking error")
}
var decisionErr *ruleengine.DecisionError
if !errors.As(err, &decisionErr) {
t.Fatalf("expected DecisionError, got %T: %v", err, err)
}
if decisionErr.Decision == nil || decisionErr.Decision.Message != "blocked" {
t.Fatalf("unexpected decision error: %#v", decisionErr.Decision)
}
if secondCalled {
t.Fatalf("second validator must not be called after blocking decision")
}
})
}
func TestGenericRulesOnCreate(t *testing.T) {
t.Parallel()
t.Run("allows when validators return nil", func(t *testing.T) {
t.Parallel()
called := false
h := &genericRules{
rules: []genericRuleValidator{
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
called = true
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
fn := h.OnCreate(
nil,
nil,
genericMetadataObject(nil, nil),
nil,
testEventRecorder{},
testTenant(),
[]*apirules.NamespaceRuleBodyNamespace{
{
Enforce: &apirules.NamespaceRuleEnforceBody{
Action: apirules.ActionTypeAllow,
},
},
},
)
resp := fn(context.Background(), admissionRequest("v1", "ConfigMap"))
if resp != nil {
t.Fatalf("expected nil response, got %#v", resp)
}
if !called {
t.Fatalf("expected validator to be called")
}
})
t.Run("denies incomplete admission kind", func(t *testing.T) {
t.Parallel()
h := &genericRules{
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
fn := h.OnCreate(
nil,
nil,
genericMetadataObject(nil, nil),
nil,
testEventRecorder{},
testTenant(),
nil,
)
resp := fn(context.Background(), admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Version: "",
Kind: "ConfigMap",
},
},
})
if resp == nil {
t.Fatalf("expected denial response")
}
if resp.Allowed {
t.Fatalf("expected denied response")
}
if !strings.Contains(resp.Result.Message, "admission request kind is incomplete") {
t.Fatalf("expected incomplete kind error, got %q", resp.Result.Message)
}
})
t.Run("denies validator error", func(t *testing.T) {
t.Parallel()
h := &genericRules{
rules: []genericRuleValidator{
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
return nil, errors.New("validator failed")
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
fn := h.OnCreate(
nil,
nil,
genericMetadataObject(nil, nil),
nil,
testEventRecorder{},
testTenant(),
nil,
)
resp := fn(context.Background(), admissionRequest("v1", "ConfigMap"))
if resp == nil {
t.Fatalf("expected denial response")
}
if resp.Allowed {
t.Fatalf("expected denied response")
}
if !strings.Contains(resp.Result.Message, "validator failed") {
t.Fatalf("expected validator error, got %q", resp.Result.Message)
}
})
}
func TestGenericRulesOnUpdate(t *testing.T) {
t.Parallel()
t.Run("uses new object and allows when validators return nil", func(t *testing.T) {
t.Parallel()
calledWithNewObject := false
oldObj := genericMetadataObject(map[string]string{"old": "true"}, nil)
newObj := genericMetadataObject(map[string]string{"new": "true"}, nil)
h := &genericRules{
rules: []genericRuleValidator{
func(obj genericObject, _ schema.GroupVersionKind, _ []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
calledWithNewObject = obj.GetLabels()["new"] == "true"
return nil, nil
},
},
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
fn := h.OnUpdate(
nil,
nil,
oldObj,
newObj,
nil,
testEventRecorder{},
testTenant(),
nil,
)
resp := fn(context.Background(), admissionRequest("v1", "ConfigMap"))
if resp != nil {
t.Fatalf("expected nil response, got %#v", resp)
}
if !calledWithNewObject {
t.Fatalf("expected validator to receive the new object")
}
})
t.Run("denies incomplete admission kind", func(t *testing.T) {
t.Parallel()
h := &genericRules{
managedMetadata: meta.NewManagedMetadata(nil, nil),
objectSkipRules: meta.DefaultObjectSkipRules(),
}
fn := h.OnUpdate(
nil,
nil,
genericMetadataObject(nil, nil),
genericMetadataObject(nil, nil),
nil,
testEventRecorder{},
testTenant(),
nil,
)
resp := fn(context.Background(), admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "",
},
},
})
if resp == nil {
t.Fatalf("expected denial response")
}
if resp.Allowed {
t.Fatalf("expected denied response")
}
if !strings.Contains(resp.Result.Message, "admission request kind is incomplete") {
t.Fatalf("expected incomplete kind error, got %q", resp.Result.Message)
}
})
}
func TestGenericRulesOnDelete(t *testing.T) {
t.Parallel()
h := &genericRules{}
fn := h.OnDelete(
nil,
nil,
genericMetadataObject(nil, nil),
nil,
testEventRecorder{},
testTenant(),
nil,
)
resp := fn(context.Background(), admissionRequest("v1", "ConfigMap"))
if resp != nil {
t.Fatalf("expected nil response, got %#v", resp)
}
}
func TestGroupVersionKind(t *testing.T) {
t.Parallel()
tests := []struct {
name string
req admission.Request
want schema.GroupVersionKind
wantErr string
}{
{
name: "core v1 kind",
req: admissionRequest("v1", "ConfigMap"),
want: schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "ConfigMap",
},
},
{
name: "grouped kind",
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
},
},
want: schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
},
{
name: "missing version",
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: "",
Version: "",
Kind: "ConfigMap",
},
},
},
wantErr: "admission request kind is incomplete",
},
{
name: "missing kind",
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "",
},
},
},
wantErr: "admission request kind is incomplete",
},
{
name: "empty request",
req: admission.Request{},
wantErr: "admission request kind is incomplete",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := groupVersionKind(tt.req)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got %v", tt.wantErr, err)
}
return
}
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got != tt.want {
t.Fatalf("expected %s, got %s", tt.want.String(), got.String())
}
})
}
}
func genericMetadataObject(
labels map[string]string,
annotations map[string]string,
) genericObject {
return &metav1.PartialObjectMetadata{
ObjectMeta: metav1.ObjectMeta{
Name: "object",
Namespace: "tenant-a",
Labels: labels,
Annotations: annotations,
},
}
}
var (
_ events.EventRecorder = testEventRecorder{}
_ events.LabeledEvent = (*testLabeledEvent)(nil)
)
type testEventRecorder struct{}
func (testEventRecorder) Eventf(
k8sruntime.Object,
k8sruntime.Object,
string,
string,
string,
string,
...interface{},
) {
}
func (testEventRecorder) LabeledEvent(
regarding k8sruntime.Object,
eventType string,
reason string,
action string,
note string,
) events.LabeledEvent {
return &testLabeledEvent{
regarding: regarding,
eventType: eventType,
reason: reason,
action: action,
note: note,
labels: map[string]string{},
annotations: map[string]string{},
}
}
type testLabeledEvent struct {
regarding k8sruntime.Object
related k8sruntime.Object
eventType string
reason string
action string
note string
labels map[string]string
annotations map[string]string
}
func (*testLabeledEvent) Emit(context.Context) {}
func (e *testLabeledEvent) WithRelated(obj k8sruntime.Object) events.LabeledEvent {
e.related = obj
return e
}
func (e *testLabeledEvent) WithLabels(labels map[string]string) events.LabeledEvent {
for key, value := range labels {
e.labels[key] = value
}
return e
}
func (e *testLabeledEvent) WithAnnotations(annotations map[string]string) events.LabeledEvent {
for key, value := range annotations {
e.annotations[key] = value
}
return e
}
func (e *testLabeledEvent) WithTenantLabel(tnt *capsulev1beta2.Tenant) events.LabeledEvent {
if tnt != nil {
e.labels[meta.NewTenantLabel] = tnt.Name
}
return e
}
func (e *testLabeledEvent) WithRequestAnnotations(req admission.Request) events.LabeledEvent {
if req.UID != "" {
e.annotations[meta.AuditRequestUID] = string(req.UID)
}
if req.UserInfo.Username != "" {
e.annotations[meta.AuditUsername] = req.UserInfo.Username
}
return e
}
func (e *testLabeledEvent) Reason() string {
return e.reason
}
func (e *testLabeledEvent) Action() string {
return e.action
}
func (e *testLabeledEvent) Regarding() k8sruntime.Object {
return e.regarding
}
func (e *testLabeledEvent) Labels() map[string]string {
return e.labels
}
func (e *testLabeledEvent) Annotations() map[string]string {
return e.annotations
}
func (e *testLabeledEvent) Note() string {
return e.note
}
func (e *testLabeledEvent) EventType() string {
return e.eventType
}
func (e *testLabeledEvent) Related() k8sruntime.Object {
return e.related
}
func admissionRequest(
apiVersion string,
kind string,
) admission.Request {
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
panic(err)
}
return admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gv.Group,
Version: gv.Version,
Kind: kind,
},
},
}
}
func testTenant() *capsulev1beta2.Tenant {
return &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-a",
},
}
}
func TestGenericRulesWithRealMetadataValidatorSmoke(t *testing.T) {
t.Parallel()
h := GenericRules(cache.NewRegexCache()).(*genericRules)
evaluation, err := h.validateMetadata(
genericMetadataObject(map[string]string{"env": "prod"}, nil),
schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "ConfigMap",
},
[]*apirules.NamespaceRuleEnforceBody{
{
Action: apirules.ActionTypeAllow,
Metadata: []apirules.MetadataRule{
{
VersionKinds: gvkVersionKinds([]string{"*"}, "ConfigMap"),
Labels: map[string]apirules.MetadataValueRule{
"env": {
Required: true,
Values: []runtime.ExpressionMatch{
{
Exact: []string{"prod"},
},
},
},
},
},
},
},
},
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if evaluation == nil {
t.Fatalf("expected evaluation")
}
if err := evaluation.BlockingError(); err != nil {
t.Fatalf("expected no blocking error, got %v", err)
}
}
func gvkVersionKinds(apiVersion []string, kinds ...string) runtime.VersionKinds {
return runtime.VersionKinds{
APIGroups: apiVersion,
Kinds: kinds,
}
}