Files
capsule/pkg/ruleengine/validate_test.go
T
Oliver BählerandGitHub f1b760a87a feat(rules): improve metadata enforcement and add ingress rules (#2050)
* feat: implement namespace metadata enforcement

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

* feat: add ingress enforcment

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

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-07-22 22:24:54 +02:00

1254 lines
28 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package ruleengine
import (
"strings"
"testing"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/ptr"
"github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/api/runtime"
)
func TestValidateRuleStatusBody(t *testing.T) {
t.Parallel()
mapper := newRuleValidationRESTMapper()
tests := []struct {
name string
bodies []*rules.NamespaceRuleBodyNamespace
mapper apimeta.RESTMapper
wantErr string
}{
{
name: "nil bodies are valid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
nil,
{},
{
Enforce: nil,
},
},
},
{
name: "valid workload service and metadata rules",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeAllow,
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"ConfigMap",
"Service",
"Deployment",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
Values: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "^(prod|test|dev)$",
},
},
{
Exact: []string{
"prod",
"test",
},
},
},
},
"presence-only": {
Required: true,
},
},
Annotations: map[string]rules.MetadataValueRule{
"example.corp/cost-center": {
Required: false,
Values: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "^INV-[0-9]{4}$",
},
},
{
Exact: []string{
"prod",
"test",
},
},
},
},
},
},
},
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
Registries: []rules.OCIRegistry{
{
ExpressionMatch: runtime.ExpressionMatch{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "harbor/.*",
},
},
},
{
ExpressionMatch: runtime.ExpressionMatch{
Exact: []string{
"harbor/platform/debian:latest",
},
},
},
},
Schedulers: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "tenant-[a-z0-9-]+",
},
},
},
},
Services: rules.NamespaceRuleEnforceServicesBody{
Types: []rules.ServiceType{
rules.ServiceTypeClusterIP,
rules.ServiceTypeNodePort,
rules.ServiceTypeLoadBalancer,
rules.ServiceTypeExternalName,
},
LoadBalancers: &rules.ServiceLoadBalancerRule{
CIDRs: []string{
"10.0.0.2/32",
"10.0.1.0/24",
"2001:db8::/32",
"10.0.0.3",
},
},
ExternalNames: &rules.ServiceExternalNameRule{
Hostnames: []runtime.ExpressionMatch{
{
Exact: []string{
"internal.git.com",
},
},
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: ".*\\.example\\.com",
},
},
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "trusted\\..*",
Negate: true,
},
},
},
},
NodePorts: &rules.ServiceNodePortRule{
Ports: []rules.ServiceNodePortRange{
{
From: 30000,
To: 32767,
},
{
From: 30500,
To: 30500,
},
},
},
},
},
},
},
},
{
name: "valid metadata rule with empty apiVersion meaning core v1",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeAllow,
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"v1",
},
Kinds: []string{
"ConfigMap",
"Service",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
Values: []runtime.ExpressionMatch{
{
Exact: []string{
"prod",
},
},
},
},
},
},
},
},
},
},
},
{
name: "valid metadata rule with wildcard apiVersion and wildcard kind",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeAudit,
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"*",
},
},
Annotations: map[string]rules.MetadataValueRule{
"example.corp/audit": {
Values: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "^audit-.*",
},
},
},
},
},
},
},
},
},
},
},
{
name: "valid metadata rule with partial wildcards",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeAllow,
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"apps/*",
},
Kinds: []string{
"*Set",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
Values: []runtime.ExpressionMatch{
{
Exact: []string{
"prod",
},
},
},
},
},
},
},
},
},
},
},
{
name: "invalid metadata label regex",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"ConfigMap",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
Values: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "[",
},
},
},
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.metadata[0].labels["env"].values[0].exp "[" is invalid`,
},
{
name: "invalid metadata annotation regex",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"ConfigMap",
},
},
Annotations: map[string]rules.MetadataValueRule{
"example.corp/cost-center": {
Values: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "[",
},
},
},
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.metadata[0].annotations["example.corp/cost-center"].values[0].exp "[" is invalid`,
},
{
name: "invalid metadata label key",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"ConfigMap",
},
},
Labels: map[string]rules.MetadataValueRule{
"bad/key/again": {
Required: true,
Values: []runtime.ExpressionMatch{
{
Exact: []string{
"prod",
},
},
},
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.metadata[0].labels["bad/key/again"] is invalid`,
},
{
name: "invalid metadata annotation key",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"ConfigMap",
},
},
Annotations: map[string]rules.MetadataValueRule{
"bad/key/again": {
Values: []runtime.ExpressionMatch{
{
Exact: []string{
"value",
},
},
},
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.metadata[0].annotations["bad/key/again"] is invalid`,
},
{
name: "reports correct metadata indexes across multiple rules and values",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"ConfigMap",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Values: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "valid-.*",
},
},
},
},
},
},
},
},
},
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"Service",
},
},
Labels: map[string]rules.MetadataValueRule{
"team": {
Values: []runtime.ExpressionMatch{
{
Exact: []string{
"platform",
},
},
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "[",
},
},
},
},
},
},
},
},
},
},
wantErr: `rules[1].enforce.metadata[0].labels["team"].values[1].exp "[" is invalid`,
},
{
name: "invalid workload registry regex",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
Registries: []rules.OCIRegistry{
{
ExpressionMatch: runtime.ExpressionMatch{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "[",
},
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.workloads.registries[0].exp "[" is invalid`,
},
{
name: "invalid workload scheduler regex",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
Schedulers: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "[",
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.workloads.schedulers[0].exp "[" is invalid`,
},
{
name: "invalid service type",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
Types: []rules.ServiceType{
rules.ServiceTypeClusterIP,
rules.ServiceType("InvalidType"),
},
},
},
},
},
wantErr: `rules[0].enforce.services.types[1] "InvalidType" is invalid`,
},
{
name: "invalid loadBalancer CIDR",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
LoadBalancers: &rules.ServiceLoadBalancerRule{
CIDRs: []string{
"10.0.0.0/33",
},
},
},
},
},
},
wantErr: `rules[0].enforce.services.loadBalancers.cidrs[0] "10.0.0.0/33" is invalid`,
},
{
name: "empty loadBalancer CIDR",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
LoadBalancers: &rules.ServiceLoadBalancerRule{
CIDRs: []string{
"",
},
},
},
},
},
},
wantErr: `rules[0].enforce.services.loadBalancers.cidrs[0] "" is invalid: CIDR is empty`,
},
{
name: "invalid externalName hostname regex",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
ExternalNames: &rules.ServiceExternalNameRule{
Hostnames: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "[",
},
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.services.externalNames.hostnames[0].exp "[" is invalid`,
},
{
name: "nodePort from greater than to",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
NodePorts: &rules.ServiceNodePortRule{
Ports: []rules.ServiceNodePortRange{
{
From: 32767,
To: 30000,
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.services.nodePorts.ports[0] is invalid: from 32767 must be lower than or equal to 30000`,
},
{
name: "nodePort from below valid port range",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
NodePorts: &rules.ServiceNodePortRule{
Ports: []rules.ServiceNodePortRange{
{
From: 0,
To: 30000,
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.services.nodePorts.ports[0] is invalid: from 0 must be between 1 and 65535`,
},
{
name: "nodePort to above valid port range",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
NodePorts: &rules.ServiceNodePortRule{
Ports: []rules.ServiceNodePortRange{
{
From: 30000,
To: 70000,
},
},
},
},
},
},
},
wantErr: `rules[0].enforce.services.nodePorts.ports[0] is invalid: to 70000 must be between 1 and 65535`,
},
{
name: "single nodePort range is valid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
NodePorts: &rules.ServiceNodePortRule{
Ports: []rules.ServiceNodePortRange{
{
From: 30500,
To: 30500,
},
},
},
},
},
},
},
},
{
name: "reports correct indexes across multiple service rules",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
Types: []rules.ServiceType{
rules.ServiceTypeClusterIP,
},
},
},
},
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
ExternalNames: &rules.ServiceExternalNameRule{
Hostnames: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "valid\\..*",
},
},
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "[",
},
},
},
},
},
},
},
},
wantErr: `rules[1].enforce.services.externalNames.hostnames[1].exp "[" is invalid`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := ValidateRuleStatusBody(tt.mapper, tt.bodies)
if tt.wantErr == "" {
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error())
}
})
}
}
func TestValidateMetadataKeyPatterns(t *testing.T) {
t.Parallel()
if err := validateMetadataKey("example.corp/*"); err != nil {
t.Fatalf("expected wildcard key selector to be valid: %v", err)
}
if err := validateMetadataKey(`example\.corp/.*`); err != nil {
t.Fatalf("expected regexp key selector to be valid: %v", err)
}
if err := validateMetadataKey("example.corp/["); err == nil {
t.Fatal("expected invalid regexp key selector to be rejected")
}
}
func TestMutableMetadataRequiresConcreteKey(t *testing.T) {
t.Parallel()
policy := rules.MetadataValueRule{Managed: ptr.To("controlled")}
if err := validateMutableMetadataKey("example.corp/key", policy); err != nil {
t.Fatalf("concrete key rejected: %v", err)
}
if err := validateMutableMetadataKey("example.corp/*", policy); err == nil {
t.Fatal("wildcard managed key was accepted")
}
if err := validateMutableMetadataKey(`example\.corp/.*`, policy); err == nil {
t.Fatal("regexp managed key was accepted")
}
rule := rules.MetadataRule{
VersionKinds: runtime.VersionKinds{APIGroups: []string{"*"}, Kinds: []string{"ConfigMap"}},
Labels: map[string]rules.MetadataValueRule{"example.corp/key": policy},
}
if err := validateMetadataRules(0, []rules.MetadataRule{rule}, nil); err == nil || !strings.Contains(err.Error(), "managed metadata requires concrete apiGroups and kinds") {
t.Fatalf("wildcard managed target error = %v", err)
}
}
func TestValidateRuleStatusBodyWithRESTMapper(t *testing.T) {
t.Parallel()
mapper := newRuleValidationRESTMapper()
tests := []struct {
name string
bodies []*rules.NamespaceRuleBodyNamespace
mapper apimeta.RESTMapper
wantErr []string
}{
{
name: "nil mapper skips discovery validation",
mapper: nil,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"v1",
},
Kinds: []string{
"NotAThing",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
},
},
},
},
},
{
name: "known core v1 multiple kinds are valid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"v1",
},
Kinds: []string{
"ConfigMap",
"Service",
"Pod",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
Values: []runtime.ExpressionMatch{
{
Exact: []string{
"prod",
},
},
},
},
},
},
},
},
},
},
},
{
name: "core namespace is a valid metadata target",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{{
Enforce: &rules.NamespaceRuleEnforceBody{Metadata: []rules.MetadataRule{{
VersionKinds: runtime.VersionKinds{APIGroups: []string{""}, Kinds: []string{"Namespace"}},
Annotations: map[string]rules.MetadataValueRule{
"example.corp/*": {Values: []runtime.ExpressionMatch{{Exact: []string{"allowed"}}}},
},
}}},
}},
},
{
name: "known grouped apiVersion kind is valid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"apps/v1",
},
Kinds: []string{
"Deployment",
"StatefulSet",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
},
},
},
},
},
{
name: "unknown core kind is invalid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"v1",
},
Kinds: []string{
"NotAThing",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
},
},
},
},
wantErr: []string{
`rules[0].enforce.metadata[0].kinds[0]`,
`NotAThing`,
},
},
{
name: "wrong apiVersion kind combination is invalid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"v1",
},
Kinds: []string{
"Deployment",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
},
},
},
},
wantErr: []string{
`rules[0].enforce.metadata[0].kinds[0]`,
`Deployment`,
},
},
{
name: "unknown grouped kind is invalid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"apps/v1",
},
Kinds: []string{
"NotADeployment",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
},
},
},
},
wantErr: []string{
`rules[0].enforce.metadata[0].kinds[0]`,
`NotADeployment`,
},
},
{
name: "wildcard apiVersion skips discovery validation",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"*",
},
Kinds: []string{
"NotAThing",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
},
},
},
},
},
{
name: "wildcard kind skips discovery validation",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"v1",
},
Kinds: []string{
"*",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
},
},
},
},
},
{
name: "partial wildcard kind skips discovery validation",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"apps/v1",
},
Kinds: []string{
"*Set",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
},
},
},
},
},
{
name: "reports correct indexes across multiple metadata rules and kinds",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"v1",
},
Kinds: []string{
"ConfigMap",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
},
},
},
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"apps/v1",
},
Kinds: []string{
"Deployment",
"NotADeployment",
},
},
Labels: map[string]rules.MetadataValueRule{
"team": {
Required: true,
},
},
},
},
},
},
},
wantErr: []string{
`rules[0].enforce.metadata[1].kinds[1]`,
`NotADeployment`,
},
},
{
name: "still validates metadata syntax when mapper is enabled",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{
"v1",
},
Kinds: []string{
"ConfigMap",
},
},
Labels: map[string]rules.MetadataValueRule{
"env": {
Required: true,
Values: []runtime.ExpressionMatch{
{
ExpressionRegex: runtime.ExpressionRegex{
Expression: "[",
},
},
},
},
},
},
},
},
},
},
wantErr: []string{
`rules[0].enforce.metadata[0].labels["env"].values[0].exp "[" is invalid`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := ValidateRuleStatusBody(tt.mapper, tt.bodies)
if len(tt.wantErr) == 0 {
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
}
for _, expected := range tt.wantErr {
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected error containing %q, got %q", expected, err.Error())
}
}
})
}
}
func newRuleValidationRESTMapper() apimeta.RESTMapper {
mapper := apimeta.NewDefaultRESTMapper([]schema.GroupVersion{
{
Group: "",
Version: "v1",
},
{
Group: "apps",
Version: "v1",
},
})
mapper.Add(
schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Namespace",
},
apimeta.RESTScopeRoot,
)
mapper.Add(
schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "ConfigMap",
},
apimeta.RESTScopeNamespace,
)
mapper.Add(
schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Service",
},
apimeta.RESTScopeNamespace,
)
mapper.Add(
schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Pod",
},
apimeta.RESTScopeNamespace,
)
mapper.Add(
schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
apimeta.RESTScopeNamespace,
)
mapper.Add(
schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "StatefulSet",
},
apimeta.RESTScopeNamespace,
)
return mapper
}