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>
This commit is contained in:
Oliver Bähler
2026-07-02 13:42:08 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 6fbd472f27
commit 77d1810bb9
89 changed files with 12731 additions and 556 deletions
+141
View File
@@ -0,0 +1,141 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package runtime
import (
"fmt"
"regexp"
"slices"
"strings"
)
// At least one of Exact or Exp must be set.
// Both may be set together.
// +kubebuilder:object:generate=true
// +kubebuilder:validation:XValidation:rule="has(self.exact) || has(self.exp)",message="at least one of exact or exp must be set"
type ExpressionMatch struct {
ExpressionRegex `json:",inline"`
// Exact matches one of the provided values exactly.
//
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:Items:MinLength=1
// +optional
Exact []string `json:"exact,omitempty"`
}
type ExpressionRegex struct {
// Exp matches regular expression.
//
// +kubebuilder:validation:MinLength=1
// +optional
Expression string `json:"exp,omitempty"`
// Negate regular Expression
//+kubebuilder:default:=false
Negate bool `json:"negate,omitempty"`
}
type ExpressionRegexMatcher interface {
MatchRegex(expression ExpressionRegex, value string) (bool, error)
}
func (m ExpressionMatch) Matches(value string) (bool, error) {
matched, err := m.matches(value)
if err != nil {
return false, err
}
return m.applyNegate(matched), nil
}
func (m ExpressionMatch) MatchesWithExpressionMatcher(
matcher ExpressionRegexMatcher,
value string,
) (bool, error) {
if len(m.Exact) == 0 && m.Expression == "" {
return false, fmt.Errorf("expression match must define at least one of exact or exp")
}
matched := containsExact(m.Exact, value)
if matched {
return m.applyNegate(true), nil
}
if m.Expression == "" {
return m.applyNegate(false), nil
}
if matcher == nil {
return m.Matches(value)
}
matched, err := matcher.MatchRegex(m.ExpressionRegex, value)
if err != nil {
return false, err
}
// Important: assume MatchRegex already applies ExpressionRegex.Negate.
// If your RegexCache.MatchRegex already handles Negate, return directly.
return matched, nil
}
func (m ExpressionMatch) Describe() string {
return DescribeExpressionMatch(m)
}
func (m ExpressionMatch) matches(value string) (bool, error) {
if len(m.Exact) == 0 && m.Expression == "" {
return false, fmt.Errorf("expression match must define at least one of exact or exp")
}
if containsExact(m.Exact, value) {
return true, nil
}
if m.Expression == "" {
return false, nil
}
re, err := regexp.Compile(m.Expression)
if err != nil {
return false, fmt.Errorf("compile regexp %q: %w", m.Expression, err)
}
return re.MatchString(value), nil
}
func containsExact(values []string, value string) bool {
return slices.Contains(values, value)
}
func (m ExpressionMatch) applyNegate(matched bool) bool {
if m.Negate {
return !matched
}
return matched
}
func DescribeExpressionMatch(match ExpressionMatch) string {
parts := make([]string, 0, 3)
prefix := ""
if match.Negate {
prefix = "not "
}
if len(match.Exact) > 0 {
parts = append(parts, fmt.Sprintf("%sexact: %s", prefix, strings.Join(match.Exact, ", ")))
}
if match.Expression != "" {
parts = append(parts, fmt.Sprintf("%sexp: %s", prefix, match.Expression))
}
if len(parts) == 0 && match.Negate {
return "not <empty>"
}
return strings.Join(parts, "; ")
}
+917
View File
@@ -0,0 +1,917 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package runtime
import (
"errors"
"fmt"
"regexp"
"testing"
)
type fakeExpressionRegexMatcher struct {
t *testing.T
calls int
err error
matches map[string]bool
seen []ExpressionRegex
}
func (m *fakeExpressionRegexMatcher) MatchRegex(expr ExpressionRegex, value string) (bool, error) {
m.t.Helper()
m.calls++
m.seen = append(m.seen, expr)
if m.err != nil {
return false, m.err
}
key := fmt.Sprintf("%s|%t|%s", expr.Expression, expr.Negate, value)
if matched, ok := m.matches[key]; ok {
return matched, nil
}
re, err := regexp.Compile(expr.Expression)
if err != nil {
return false, err
}
matched := re.MatchString(value)
if expr.Negate {
return !matched, nil
}
return matched, nil
}
func TestExpressionMatch_Matches(t *testing.T) {
t.Parallel()
tests := []struct {
name string
match ExpressionMatch
value string
wantMatch bool
wantErr bool
}{
{
name: "exact matches single value",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
},
value: "default-scheduler",
wantMatch: true,
},
{
name: "exact does not match different value",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
},
value: "custom-scheduler",
wantMatch: false,
},
{
name: "exact matches one of multiple values",
match: ExpressionMatch{
Exact: []string{"default-scheduler", "custom-scheduler", "team-scheduler"},
},
value: "custom-scheduler",
wantMatch: true,
},
{
name: "exact does not match any of multiple values",
match: ExpressionMatch{
Exact: []string{"default-scheduler", "custom-scheduler"},
},
value: "other-scheduler",
wantMatch: false,
},
{
name: "exact is case sensitive",
match: ExpressionMatch{
Exact: []string{"Default-Scheduler"},
},
value: "default-scheduler",
wantMatch: false,
},
{
name: "exact uses literal string not pattern",
match: ExpressionMatch{
Exact: []string{"team-.*"},
},
value: "team-a",
wantMatch: false,
},
{
name: "exact matches literal pattern string",
match: ExpressionMatch{
Exact: []string{"team-.*"},
},
value: "team-.*",
wantMatch: true,
},
{
name: "exact with empty value can match empty string when present",
match: ExpressionMatch{
Exact: []string{""},
},
value: "",
wantMatch: true,
},
{
name: "regex matches value",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^team-[a-z0-9-]+$",
},
},
value: "team-alpha-1",
wantMatch: true,
},
{
name: "regex does not match value",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^team-[a-z0-9-]+$",
},
},
value: "kube-scheduler",
wantMatch: false,
},
{
name: "regex is not implicitly anchored",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "team",
},
},
value: "my-team-scheduler",
wantMatch: true,
},
{
name: "invalid regex returns error",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "[",
},
},
value: "team-alpha",
wantErr: true,
},
{
name: "combined exact and regex matches by exact",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Expression: "^team-[a-z0-9-]+$",
},
},
value: "default-scheduler",
wantMatch: true,
},
{
name: "combined exact and regex matches by regex",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Expression: "^team-[a-z0-9-]+$",
},
},
value: "team-alpha",
wantMatch: true,
},
{
name: "combined exact and regex does not match either",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Expression: "^team-[a-z0-9-]+$",
},
},
value: "other-scheduler",
wantMatch: false,
},
{
name: "combined exact match skips invalid regex",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Expression: "[",
},
},
value: "default-scheduler",
wantMatch: true,
},
{
name: "combined exact miss evaluates invalid regex and returns error",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Expression: "[",
},
},
value: "team-alpha",
wantErr: true,
},
{
name: "negated exact matching value returns false",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Negate: true,
},
},
value: "default-scheduler",
wantMatch: false,
},
{
name: "negated exact non matching value returns true",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Negate: true,
},
},
value: "custom-scheduler",
wantMatch: true,
},
{
name: "negated exact with multiple values matching one returns false",
match: ExpressionMatch{
Exact: []string{"default-scheduler", "custom-scheduler"},
ExpressionRegex: ExpressionRegex{
Negate: true,
},
},
value: "custom-scheduler",
wantMatch: false,
},
{
name: "negated regex matching value returns false",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^trusted/.*",
Negate: true,
},
},
value: "trusted/platform/app:1",
wantMatch: false,
},
{
name: "negated regex non matching value returns true",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^trusted/.*",
Negate: true,
},
},
value: "docker.io/library/nginx:latest",
wantMatch: true,
},
{
name: "negated combined exact match returns false",
match: ExpressionMatch{
Exact: []string{"trusted/platform/app:1"},
ExpressionRegex: ExpressionRegex{
Expression: "^trusted/.+",
Negate: true,
},
},
value: "trusted/platform/app:1",
wantMatch: false,
},
{
name: "negated combined regex match returns false",
match: ExpressionMatch{
Exact: []string{"trusted/platform/app:1"},
ExpressionRegex: ExpressionRegex{
Expression: "^trusted/.+",
Negate: true,
},
},
value: "trusted/other/app:1",
wantMatch: false,
},
{
name: "negated combined no match returns true",
match: ExpressionMatch{
Exact: []string{"trusted/platform/app:1"},
ExpressionRegex: ExpressionRegex{
Expression: "^trusted/.+",
Negate: true,
},
},
value: "harbor/platform/app:1",
wantMatch: true,
},
{
name: "empty matcher returns error",
match: ExpressionMatch{},
value: "anything",
wantErr: true,
wantMatch: false,
},
{
name: "empty exact slice with empty regex returns error",
match: ExpressionMatch{
Exact: []string{},
},
value: "anything",
wantErr: true,
},
{
name: "nil exact with whitespace regex is treated as regex and does not match",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: " ",
},
},
value: "anything",
wantMatch: false,
},
{
name: "duplicate exact values still match",
match: ExpressionMatch{
Exact: []string{"a", "a", "b"},
},
value: "a",
wantMatch: true,
},
{
name: "exact values are not trimmed",
match: ExpressionMatch{
Exact: []string{" value "},
},
value: "value",
wantMatch: false,
},
{
name: "exact values match with spaces when value has spaces",
match: ExpressionMatch{
Exact: []string{" value "},
},
value: " value ",
wantMatch: true,
},
{
name: "regex can match empty value",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^$",
},
},
value: "",
wantMatch: true,
},
{
name: "negated regex can reject empty value",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^$",
Negate: true,
},
},
value: "",
wantMatch: false,
},
{
name: "negated regex can match non empty value against empty regex",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^$",
Negate: true,
},
},
value: "non-empty",
wantMatch: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := tt.match.Matches(tt.value)
if tt.wantErr {
if err == nil {
t.Fatalf("Matches() expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Matches() unexpected error: %v", err)
}
if got != tt.wantMatch {
t.Fatalf("Matches() = %t, want %t", got, tt.wantMatch)
}
})
}
}
func TestExpressionMatch_MatchesWithExpressionMatcher_NilMatcherFallback(t *testing.T) {
t.Parallel()
tests := []struct {
name string
match ExpressionMatch
value string
wantMatch bool
wantErr bool
}{
{
name: "nil matcher exact match",
match: ExpressionMatch{
Exact: []string{"default-scheduler"},
},
value: "default-scheduler",
wantMatch: true,
},
{
name: "nil matcher regex match",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^team-.*",
},
},
value: "team-a",
wantMatch: true,
},
{
name: "nil matcher negated regex non match returns true",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^trusted/.*",
Negate: true,
},
},
value: "docker.io/library/nginx:latest",
wantMatch: true,
},
{
name: "nil matcher invalid regex returns error",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "[",
},
},
value: "team-a",
wantErr: true,
},
{
name: "nil matcher empty expression match returns error",
match: ExpressionMatch{},
value: "team-a",
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := tt.match.MatchesWithExpressionMatcher(nil, tt.value)
if tt.wantErr {
if err == nil {
t.Fatalf("MatchesWithExpressionMatcher(nil) expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("MatchesWithExpressionMatcher(nil) unexpected error: %v", err)
}
if got != tt.wantMatch {
t.Fatalf("MatchesWithExpressionMatcher(nil) = %t, want %t", got, tt.wantMatch)
}
})
}
}
func TestExpressionMatch_MatchesWithExpressionMatcher_UsesMatcherForRegex(t *testing.T) {
t.Parallel()
matcher := &fakeExpressionRegexMatcher{
t: t,
matches: map[string]bool{
"^team-.*|false|team-a": true,
},
}
match := ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^team-.*",
},
}
got, err := match.MatchesWithExpressionMatcher(matcher, "team-a")
if err != nil {
t.Fatalf("MatchesWithExpressionMatcher() unexpected error: %v", err)
}
if !got {
t.Fatalf("MatchesWithExpressionMatcher() = false, want true")
}
if matcher.calls != 1 {
t.Fatalf("MatchRegex() calls = %d, want 1", matcher.calls)
}
if len(matcher.seen) != 1 {
t.Fatalf("seen expressions = %d, want 1", len(matcher.seen))
}
if matcher.seen[0].Expression != "^team-.*" {
t.Fatalf("seen expression = %q, want %q", matcher.seen[0].Expression, "^team-.*")
}
if matcher.seen[0].Negate {
t.Fatalf("seen negate = true, want false")
}
}
func TestExpressionMatch_MatchesWithExpressionMatcher_PassesNegateToMatcher(t *testing.T) {
t.Parallel()
matcher := &fakeExpressionRegexMatcher{
t: t,
matches: map[string]bool{
"^trusted/.*|true|docker.io/library/nginx:latest": true,
},
}
match := ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^trusted/.*",
Negate: true,
},
}
got, err := match.MatchesWithExpressionMatcher(matcher, "docker.io/library/nginx:latest")
if err != nil {
t.Fatalf("MatchesWithExpressionMatcher() unexpected error: %v", err)
}
if !got {
t.Fatalf("MatchesWithExpressionMatcher() = false, want true")
}
if matcher.calls != 1 {
t.Fatalf("MatchRegex() calls = %d, want 1", matcher.calls)
}
if len(matcher.seen) != 1 {
t.Fatalf("seen expressions = %d, want 1", len(matcher.seen))
}
if !matcher.seen[0].Negate {
t.Fatalf("seen negate = false, want true")
}
}
func TestExpressionMatch_MatchesWithExpressionMatcher_DoesNotUseMatcherWhenExactMatches(t *testing.T) {
t.Parallel()
matcher := &fakeExpressionRegexMatcher{
t: t,
err: errors.New("matcher should not be called"),
}
match := ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Expression: "[",
},
}
got, err := match.MatchesWithExpressionMatcher(matcher, "default-scheduler")
if err != nil {
t.Fatalf("MatchesWithExpressionMatcher() unexpected error: %v", err)
}
if !got {
t.Fatalf("MatchesWithExpressionMatcher() = false, want true")
}
if matcher.calls != 0 {
t.Fatalf("MatchRegex() calls = %d, want 0", matcher.calls)
}
}
func TestExpressionMatch_MatchesWithExpressionMatcher_UsesMatcherWhenExactDoesNotMatch(t *testing.T) {
t.Parallel()
matcher := &fakeExpressionRegexMatcher{
t: t,
matches: map[string]bool{
"^team-.*|false|team-a": true,
},
}
match := ExpressionMatch{
Exact: []string{"default-scheduler"},
ExpressionRegex: ExpressionRegex{
Expression: "^team-.*",
},
}
got, err := match.MatchesWithExpressionMatcher(matcher, "team-a")
if err != nil {
t.Fatalf("MatchesWithExpressionMatcher() unexpected error: %v", err)
}
if !got {
t.Fatalf("MatchesWithExpressionMatcher() = false, want true")
}
if matcher.calls != 1 {
t.Fatalf("MatchRegex() calls = %d, want 1", matcher.calls)
}
}
func TestExpressionMatch_MatchesWithExpressionMatcher_ReturnsMatcherError(t *testing.T) {
t.Parallel()
wantErr := errors.New("compile failed")
matcher := &fakeExpressionRegexMatcher{
t: t,
err: wantErr,
}
match := ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^team-.*",
},
}
got, err := match.MatchesWithExpressionMatcher(matcher, "team-a")
if err == nil {
t.Fatalf("MatchesWithExpressionMatcher() expected error, got nil")
}
if !errors.Is(err, wantErr) {
t.Fatalf("MatchesWithExpressionMatcher() error = %v, want %v", err, wantErr)
}
if got {
t.Fatalf("MatchesWithExpressionMatcher() = true, want false on error")
}
if matcher.calls != 1 {
t.Fatalf("MatchRegex() calls = %d, want 1", matcher.calls)
}
}
func TestExpressionMatch_MatchesAndMatchesWithExpressionMatcher_AgreeForNilMatcher(t *testing.T) {
t.Parallel()
tests := []struct {
name string
match ExpressionMatch
value string
}{
{
name: "exact only",
match: ExpressionMatch{
Exact: []string{"a", "b"},
},
value: "a",
},
{
name: "regex only",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^a+$",
},
},
value: "aaa",
},
{
name: "combined exact and regex exact wins",
match: ExpressionMatch{
Exact: []string{"a"},
ExpressionRegex: ExpressionRegex{
Expression: "^b+$",
},
},
value: "a",
},
{
name: "combined exact and regex regex wins",
match: ExpressionMatch{
Exact: []string{"a"},
ExpressionRegex: ExpressionRegex{
Expression: "^b+$",
},
},
value: "bbb",
},
{
name: "negated exact",
match: ExpressionMatch{
Exact: []string{"a"},
ExpressionRegex: ExpressionRegex{
Negate: true,
},
},
value: "b",
},
{
name: "negated regex",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "^a+$",
Negate: true,
},
},
value: "bbb",
},
{
name: "invalid regex",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Expression: "[",
},
},
value: "a",
},
{
name: "empty matcher",
match: ExpressionMatch{},
value: "a",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotMatches, errMatches := tt.match.Matches(tt.value)
gotWithMatcher, errWithMatcher := tt.match.MatchesWithExpressionMatcher(nil, tt.value)
if (errMatches != nil) != (errWithMatcher != nil) {
t.Fatalf(
"error mismatch: Matches() err=%v, MatchesWithExpressionMatcher(nil) err=%v",
errMatches,
errWithMatcher,
)
}
if gotMatches != gotWithMatcher {
t.Fatalf(
"result mismatch: Matches()=%t, MatchesWithExpressionMatcher(nil)=%t",
gotMatches,
gotWithMatcher,
)
}
})
}
}
func TestContainsExact(t *testing.T) {
t.Parallel()
tests := []struct {
name string
values []string
value string
want bool
}{
{
name: "nil values",
values: nil,
value: "a",
want: false,
},
{
name: "empty values",
values: []string{},
value: "a",
want: false,
},
{
name: "contains value",
values: []string{"a", "b", "c"},
value: "b",
want: true,
},
{
name: "does not contain value",
values: []string{"a", "b", "c"},
value: "d",
want: false,
},
{
name: "case sensitive",
values: []string{"A"},
value: "a",
want: false,
},
{
name: "empty string",
values: []string{""},
value: "",
want: true,
},
{
name: "whitespace is significant",
values: []string{" a "},
value: "a",
want: false,
},
{
name: "whitespace matches exactly",
values: []string{" a "},
value: " a ",
want: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := containsExact(tt.values, tt.value)
if got != tt.want {
t.Fatalf("containsExact(%v, %q) = %t, want %t", tt.values, tt.value, got, tt.want)
}
})
}
}
func TestExpressionMatch_applyNegate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
match ExpressionMatch
matched bool
want bool
}{
{
name: "non negated true",
match: ExpressionMatch{},
matched: true,
want: true,
},
{
name: "non negated false",
match: ExpressionMatch{},
matched: false,
want: false,
},
{
name: "negated true",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Negate: true,
},
},
matched: true,
want: false,
},
{
name: "negated false",
match: ExpressionMatch{
ExpressionRegex: ExpressionRegex{
Negate: true,
},
},
matched: false,
want: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := tt.match.applyNegate(tt.matched)
if got != tt.want {
t.Fatalf("applyNegate(%t) = %t, want %t", tt.matched, got, tt.want)
}
})
}
}
+440
View File
@@ -0,0 +1,440 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package runtime
import (
"fmt"
"strings"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
WildcardVersionKindMatcher = "*"
CoreAPIVersion = "v1"
)
// +kubebuilder:object:generate=true
type VersionKind struct {
// Kind of the referent.
//
// Use "*" to match all kinds.
//
// +kubebuilder:validation:MinLength=1
Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"`
// API version, API group, or API group/version selector of the referent.
//
// Empty APIVersion means the core Kubernetes API version "v1".
// Use "*" to explicitly match all API groups and versions.
//
// Examples:
// - "" means core "v1".
// - "v1" means core "v1".
// - "apps" means any version in the "apps" API group.
// - "apps/v1" means the "apps/v1" API group/version.
// - "apps/*" means any version in the "apps" API group.
//
// +optional
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,5,opt,name=apiVersion"`
}
func (s VersionKind) GroupVersionKind() schema.GroupVersionKind {
apiVersion := normalizeAPIVersion(s.APIVersion)
if apiVersion == CoreAPIVersion {
return schema.GroupVersionKind{
Group: "",
Version: CoreAPIVersion,
Kind: s.Kind,
}
}
if apiVersion == WildcardVersionKindMatcher {
return schema.GroupVersionKind{
Group: "",
Version: WildcardVersionKindMatcher,
Kind: s.Kind,
}
}
if strings.Contains(apiVersion, "/") {
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return schema.GroupVersionKind{
Kind: s.Kind,
}
}
return gv.WithKind(s.Kind)
}
return schema.GroupVersionKind{
Group: apiVersion,
Kind: s.Kind,
}
}
// MatchesGroupVersionKind returns true when the receiver matches the provided GVK.
//
// Matching is exact unless the receiver contains '*'.
// Empty APIVersion is treated as "v1".
// Kind must be set. Use "*" to explicitly match all kinds.
func (s VersionKind) MatchesGroupVersionKind(gvk schema.GroupVersionKind) bool {
return matchAPIGroupPattern(normalizeAPIVersion(s.APIVersion), gvk) &&
matchPattern(s.Kind, gvk.Kind)
}
// MatchesVersionKind returns true when the receiver matches another VersionKind.
//
// The receiver is interpreted as the pattern.
// The provided VersionKind is interpreted as the concrete value.
func (s VersionKind) MatchesVersionKind(value VersionKind) bool {
return s.MatchesGroupVersionKind(value.GroupVersionKind())
}
// HasWildcard returns true when APIVersion or Kind contains a wildcard matcher.
func (s VersionKind) HasWildcard() bool {
return strings.Contains(s.APIVersion, WildcardVersionKindMatcher) ||
strings.Contains(s.Kind, WildcardVersionKindMatcher)
}
// +kubebuilder:object:generate=true
type VersionKinds struct {
// API groups or API group/version selectors of the referents.
//
// Empty or omitted APIGroups means the core Kubernetes API version "v1".
// Use "*" to match all API groups and versions.
//
// Examples:
// - [] or [""] means core "v1".
// - ["v1"] means core "v1".
// - ["apps"] means any version in the "apps" API group.
// - ["apps/v1"] means only "apps/v1".
// - ["apps", "batch/v1"] means any "apps" version and "batch/v1".
// - ["*"] means all API groups and versions.
//
// +optional
APIGroups []string `json:"apiGroups,omitempty"`
// Kinds of the referents.
//
// Use "*" to match all kinds.
//
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:items:MinLength=1
Kinds []string `json:"kinds"`
}
func (s VersionKinds) VersionKinds() []VersionKind {
apiGroups := s.NormalizedAPIGroups()
kinds := s.normalizedKinds()
out := make([]VersionKind, 0, len(apiGroups)*len(kinds))
for _, apiGroup := range apiGroups {
for _, kind := range kinds {
out = append(out, VersionKind{
APIVersion: apiGroupPatternToAPIVersionPattern(apiGroup),
Kind: kind,
})
}
}
return out
}
func (s VersionKinds) MatchesGroupVersionKind(gvk schema.GroupVersionKind) bool {
for _, kind := range s.normalizedKinds() {
if !matchPattern(kind, gvk.Kind) {
continue
}
for _, apiGroup := range s.NormalizedAPIGroups() {
if matchAPIGroupPattern(apiGroup, gvk) {
return true
}
}
}
return false
}
func (s VersionKinds) HasWildcard() bool {
for _, apiGroup := range s.APIGroups {
if strings.Contains(apiGroup, WildcardVersionKindMatcher) {
return true
}
}
for _, kind := range s.Kinds {
if strings.Contains(kind, WildcardVersionKindMatcher) {
return true
}
}
return false
}
// ValidateKnownKinds validates concrete apiGroup/kind or apiGroupVersion/kind combinations against the RESTMapper.
// Wildcard API groups or wildcard kinds are intentionally skipped because they are selectors,
// not concrete Kubernetes resources.
func (s VersionKinds) ValidateKnownKinds(mapper apimeta.RESTMapper, fieldPath string) error {
if mapper == nil {
return nil
}
kinds := s.normalizedKinds()
apiGroups := s.NormalizedAPIGroups()
for kindIndex, kind := range kinds {
if strings.Contains(kind, WildcardVersionKindMatcher) {
continue
}
for apiGroupIndex, apiGroup := range apiGroups {
if strings.Contains(apiGroup, WildcardVersionKindMatcher) {
continue
}
if err := validateKnownKindForAPIGroup(mapper, apiGroup, kind); err != nil {
return fmt.Errorf(
"%s.kinds[%d] %q for apiGroups[%d] %q is invalid: %w",
fieldPath,
kindIndex,
kind,
apiGroupIndex,
apiGroup,
err,
)
}
}
}
return nil
}
func (s VersionKinds) StatusAPIGroups() []string {
apiGroups := s.NormalizedAPIGroups()
if len(apiGroups) == 0 {
return []string{CoreAPIVersion}
}
out := make([]string, 0, len(apiGroups))
seen := make(map[string]struct{}, len(apiGroups))
for _, apiGroup := range apiGroups {
apiGroup = strings.TrimSpace(apiGroup)
if apiGroup == "" {
apiGroup = CoreAPIVersion
}
if _, ok := seen[apiGroup]; ok {
continue
}
seen[apiGroup] = struct{}{}
out = append(out, apiGroup)
}
if len(out) == 0 {
return []string{CoreAPIVersion}
}
return out
}
func validateKnownKindForAPIGroup(
mapper apimeta.RESTMapper,
apiGroup string,
kind string,
) error {
apiGroup = normalizeAPIVersion(apiGroup)
if apiGroup == CoreAPIVersion {
_, err := mapper.RESTMapping(
schema.GroupKind{
Group: "",
Kind: kind,
},
CoreAPIVersion,
)
return err
}
if strings.Contains(apiGroup, "/") {
gv, err := schema.ParseGroupVersion(apiGroup)
if err != nil {
return err
}
_, err = mapper.RESTMapping(
schema.GroupKind{
Group: gv.Group,
Kind: kind,
},
gv.Version,
)
return err
}
_, err := mapper.RESTMapping(
schema.GroupKind{
Group: apiGroup,
Kind: kind,
},
)
return err
}
func (s VersionKinds) NormalizedAPIGroups() []string {
if len(s.APIGroups) == 0 {
return []string{CoreAPIVersion}
}
out := make([]string, 0, len(s.APIGroups))
for _, apiGroup := range s.APIGroups {
apiGroup = strings.TrimSpace(apiGroup)
if apiGroup == "" {
apiGroup = CoreAPIVersion
}
out = append(out, apiGroup)
}
if len(out) == 0 {
return []string{CoreAPIVersion}
}
return out
}
func (s VersionKinds) normalizedKinds() []string {
if len(s.Kinds) == 0 {
return nil
}
out := make([]string, 0, len(s.Kinds))
for _, kind := range s.Kinds {
kind = strings.TrimSpace(kind)
if kind == "" {
continue
}
out = append(out, kind)
}
return out
}
func apiGroupPatternToAPIVersionPattern(apiGroup string) string {
apiGroup = normalizeAPIVersion(apiGroup)
if apiGroup == CoreAPIVersion {
return ""
}
if apiGroup == WildcardVersionKindMatcher {
return WildcardVersionKindMatcher
}
if strings.Contains(apiGroup, "/") {
return apiGroup
}
return apiGroup + "/" + WildcardVersionKindMatcher
}
func normalizeAPIVersion(apiVersion string) string {
if apiVersion == "" {
return CoreAPIVersion
}
return apiVersion
}
func matchAPIGroupPattern(pattern string, gvk schema.GroupVersionKind) bool {
pattern = normalizeAPIVersion(strings.TrimSpace(pattern))
if pattern == WildcardVersionKindMatcher {
return true
}
target := gvk.Group
if pattern == CoreAPIVersion || strings.Contains(pattern, "/") {
target = gvk.GroupVersion().String()
}
return matchPattern(pattern, target)
}
func matchPattern(pattern, value string) bool {
if pattern == WildcardVersionKindMatcher {
return true
}
if !strings.Contains(pattern, WildcardVersionKindMatcher) {
return pattern == value
}
parts := strings.Split(pattern, WildcardVersionKindMatcher)
if len(parts) == 2 {
if parts[0] == "" {
return strings.HasSuffix(value, parts[1])
}
if parts[1] == "" {
return strings.HasPrefix(value, parts[0])
}
}
idx := 0
if parts[0] != "" {
if !strings.HasPrefix(value, parts[0]) {
return false
}
idx = len(parts[0])
}
lastPartIndex := len(parts) - 1
suffix := parts[lastPartIndex]
limit := len(value)
if suffix != "" {
if !strings.HasSuffix(value, suffix) {
return false
}
limit -= len(suffix)
}
for _, part := range parts[1:lastPartIndex] {
if part == "" {
continue
}
if idx > limit {
return false
}
found := strings.Index(value[idx:limit], part)
if found < 0 {
return false
}
idx += found + len(part)
}
return true
}
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
//go:build !ignore_autogenerated
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
package runtime
import ()
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExpressionMatch) DeepCopyInto(out *ExpressionMatch) {
*out = *in
out.ExpressionRegex = in.ExpressionRegex
if in.Exact != nil {
in, out := &in.Exact, &out.Exact
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionMatch.
func (in *ExpressionMatch) DeepCopy() *ExpressionMatch {
if in == nil {
return nil
}
out := new(ExpressionMatch)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VersionKind) DeepCopyInto(out *VersionKind) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionKind.
func (in *VersionKind) DeepCopy() *VersionKind {
if in == nil {
return nil
}
out := new(VersionKind)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VersionKinds) DeepCopyInto(out *VersionKinds) {
*out = *in
if in.APIGroups != nil {
in, out := &in.APIGroups, &out.APIGroups
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Kinds != nil {
in, out := &in.Kinds, &out.Kinds
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionKinds.
func (in *VersionKinds) DeepCopy() *VersionKinds {
if in == nil {
return nil
}
out := new(VersionKinds)
in.DeepCopyInto(out)
return out
}