mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
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:
co-authored by
Copilot Autofix powered by AI
parent
6fbd472f27
commit
77d1810bb9
@@ -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, "; ")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user