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
@@ -33,6 +33,7 @@ const (
|
||||
|
||||
CreatedByCapsuleLabel = "projectcapsule.dev/created-by"
|
||||
CustomResourcesLabel = "projectcapsule.dev/custom-resources"
|
||||
ResourceOriginLabel = "projectcapsule.dev/resource-origin"
|
||||
|
||||
NewManagedByCapsuleLabel = "projectcapsule.dev/managed-by"
|
||||
ManagedByCapsuleLabel = "capsule.clastix.io/managed-by"
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package meta
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type ManagedMetadata struct {
|
||||
labels map[string]struct{}
|
||||
annotations map[string]struct{}
|
||||
annotationPrefixes []string
|
||||
}
|
||||
|
||||
func NewManagedMetadata(
|
||||
labels []string,
|
||||
annotations []string,
|
||||
) ManagedMetadata {
|
||||
m := ManagedMetadata{
|
||||
labels: stringSet(
|
||||
ResourcesLabel,
|
||||
TenantNameLabel,
|
||||
TenantLabel,
|
||||
NewTenantLabel,
|
||||
ResourcePoolLabel,
|
||||
FreezeLabel,
|
||||
OwnerPromotionLabel,
|
||||
ServiceAccountPromotionLabel,
|
||||
CordonedLabel,
|
||||
CapsuleNameLabel,
|
||||
CreatedByCapsuleLabel,
|
||||
CustomResourcesLabel,
|
||||
NewManagedByCapsuleLabel,
|
||||
ManagedByCapsuleLabel,
|
||||
LimitRangeLabel,
|
||||
NetworkPolicyLabel,
|
||||
ResourceQuotaLabel,
|
||||
RolebindingLabel,
|
||||
),
|
||||
annotations: stringSet(
|
||||
ReleaseAnnotation,
|
||||
ReconcileAnnotation,
|
||||
AvailableIngressClassesAnnotation,
|
||||
AvailableIngressClassesRegexpAnnotation,
|
||||
AvailableStorageClassesAnnotation,
|
||||
AvailableStorageClassesRegexpAnnotation,
|
||||
AllowedRegistriesAnnotation,
|
||||
AllowedRegistriesRegexpAnnotation,
|
||||
ForbiddenNamespaceLabelsAnnotation,
|
||||
ForbiddenNamespaceLabelsRegexpAnnotation,
|
||||
ForbiddenNamespaceAnnotationsAnnotation,
|
||||
ForbiddenNamespaceAnnotationsRegexpAnnotation,
|
||||
ProtectedTenantAnnotation,
|
||||
),
|
||||
annotationPrefixes: compactStrings(
|
||||
ResourceQuotaAnnotationPrefix,
|
||||
ResourceUsedAnnotationPrefix,
|
||||
),
|
||||
}
|
||||
|
||||
m.addLabels(labels...)
|
||||
m.addAnnotations(annotations...)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m ManagedMetadata) HasLabel(key string) bool {
|
||||
_, ok := m.labels[key]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m ManagedMetadata) HasAnnotation(key string) bool {
|
||||
if _, ok := m.annotations[key]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, prefix := range m.annotationPrefixes {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m ManagedMetadata) addLabels(values ...string) {
|
||||
addStrings(m.labels, values...)
|
||||
}
|
||||
|
||||
func (m ManagedMetadata) addAnnotations(values ...string) {
|
||||
addStrings(m.annotations, values...)
|
||||
}
|
||||
|
||||
func addStrings(set map[string]struct{}, values ...string) {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
set[value] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func stringSet(values ...string) map[string]struct{} {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make(map[string]struct{}, len(values))
|
||||
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
out[value] = struct{}{}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func compactStrings(values ...string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(values))
|
||||
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, value)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
type ObjectSkipRule struct {
|
||||
// Labels with values which indicate a skip condition.
|
||||
Labels map[string]string
|
||||
|
||||
// Annotations with values which indicate a skip condition.
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
func (s *ObjectSkipRule) ShouldSkip(
|
||||
labels map[string]string,
|
||||
annotations map[string]string,
|
||||
) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for key, expected := range s.Labels {
|
||||
value, ok := labels[key]
|
||||
if !ok || value != expected {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for key, expected := range s.Annotations {
|
||||
value, ok := annotations[key]
|
||||
if !ok || value != expected {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return len(s.Labels) > 0 || len(s.Annotations) > 0
|
||||
}
|
||||
|
||||
func DefaultObjectSkipRules() []ObjectSkipRule {
|
||||
return []ObjectSkipRule{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
NewManagedByCapsuleLabel: ValueController,
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: map[string]string{
|
||||
NewManagedByCapsuleLabel: ValueControllerResources,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ShouldSkipObjectByRules(
|
||||
obj *metav1.PartialObjectMetadata,
|
||||
rules []ObjectSkipRule,
|
||||
) bool {
|
||||
if obj == nil || len(rules) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
labels := obj.GetLabels()
|
||||
annotations := obj.GetAnnotations()
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.ShouldSkip(labels, annotations) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,850 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package meta
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestNewManagedMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("contains default managed labels", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := NewManagedMetadata(nil, nil)
|
||||
|
||||
for _, key := range []string{
|
||||
ResourcesLabel,
|
||||
TenantNameLabel,
|
||||
TenantLabel,
|
||||
NewTenantLabel,
|
||||
ResourcePoolLabel,
|
||||
FreezeLabel,
|
||||
OwnerPromotionLabel,
|
||||
ServiceAccountPromotionLabel,
|
||||
CordonedLabel,
|
||||
CapsuleNameLabel,
|
||||
CreatedByCapsuleLabel,
|
||||
CustomResourcesLabel,
|
||||
NewManagedByCapsuleLabel,
|
||||
ManagedByCapsuleLabel,
|
||||
LimitRangeLabel,
|
||||
NetworkPolicyLabel,
|
||||
ResourceQuotaLabel,
|
||||
RolebindingLabel,
|
||||
} {
|
||||
if !m.HasLabel(key) {
|
||||
t.Fatalf("expected managed label %q", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains default managed annotations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := NewManagedMetadata(nil, nil)
|
||||
|
||||
for _, key := range []string{
|
||||
ReleaseAnnotation,
|
||||
ReconcileAnnotation,
|
||||
AvailableIngressClassesAnnotation,
|
||||
AvailableIngressClassesRegexpAnnotation,
|
||||
AvailableStorageClassesAnnotation,
|
||||
AvailableStorageClassesRegexpAnnotation,
|
||||
AllowedRegistriesAnnotation,
|
||||
AllowedRegistriesRegexpAnnotation,
|
||||
ForbiddenNamespaceLabelsAnnotation,
|
||||
ForbiddenNamespaceLabelsRegexpAnnotation,
|
||||
ForbiddenNamespaceAnnotationsAnnotation,
|
||||
ForbiddenNamespaceAnnotationsRegexpAnnotation,
|
||||
ProtectedTenantAnnotation,
|
||||
} {
|
||||
if !m.HasAnnotation(key) {
|
||||
t.Fatalf("expected managed annotation %q", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains default managed annotation prefixes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := NewManagedMetadata(nil, nil)
|
||||
|
||||
for _, key := range []string{
|
||||
ResourceQuotaAnnotationPrefix + "cpu",
|
||||
ResourceQuotaAnnotationPrefix + "memory",
|
||||
ResourceUsedAnnotationPrefix + "cpu",
|
||||
ResourceUsedAnnotationPrefix + "memory",
|
||||
} {
|
||||
if !m.HasAnnotation(key) {
|
||||
t.Fatalf("expected managed annotation prefix match for %q", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("adds custom managed labels and annotations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := NewManagedMetadata(
|
||||
[]string{
|
||||
"example.corp/label",
|
||||
" example.corp/trimmed-label ",
|
||||
"",
|
||||
" ",
|
||||
},
|
||||
[]string{
|
||||
"example.corp/annotation",
|
||||
" example.corp/trimmed-annotation ",
|
||||
"",
|
||||
" ",
|
||||
},
|
||||
)
|
||||
|
||||
for _, key := range []string{
|
||||
"example.corp/label",
|
||||
"example.corp/trimmed-label",
|
||||
} {
|
||||
if !m.HasLabel(key) {
|
||||
t.Fatalf("expected custom managed label %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range []string{
|
||||
"example.corp/annotation",
|
||||
"example.corp/trimmed-annotation",
|
||||
} {
|
||||
if !m.HasAnnotation(key) {
|
||||
t.Fatalf("expected custom managed annotation %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
if m.HasLabel("") {
|
||||
t.Fatalf("empty label must not be managed")
|
||||
}
|
||||
|
||||
if m.HasAnnotation("") {
|
||||
t.Fatalf("empty annotation must not be managed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("matching is exact and case-sensitive", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := NewManagedMetadata(
|
||||
[]string{
|
||||
"example.corp/managed",
|
||||
},
|
||||
[]string{
|
||||
"example.corp/managed",
|
||||
},
|
||||
)
|
||||
|
||||
if !m.HasLabel("example.corp/managed") {
|
||||
t.Fatalf("expected exact label match")
|
||||
}
|
||||
|
||||
if m.HasLabel("Example.Corp/managed") {
|
||||
t.Fatalf("expected label lookup to be case-sensitive")
|
||||
}
|
||||
|
||||
if m.HasLabel(" example.corp/managed ") {
|
||||
t.Fatalf("expected label lookup not to trim lookup key")
|
||||
}
|
||||
|
||||
if !m.HasAnnotation("example.corp/managed") {
|
||||
t.Fatalf("expected exact annotation match")
|
||||
}
|
||||
|
||||
if m.HasAnnotation("Example.Corp/managed") {
|
||||
t.Fatalf("expected annotation lookup to be case-sensitive")
|
||||
}
|
||||
|
||||
if m.HasAnnotation(" example.corp/managed ") {
|
||||
t.Fatalf("expected annotation lookup not to trim lookup key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown metadata is not managed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := NewManagedMetadata(nil, nil)
|
||||
|
||||
if m.HasLabel("example.corp/not-managed") {
|
||||
t.Fatalf("unexpected managed label")
|
||||
}
|
||||
|
||||
if m.HasAnnotation("example.corp/not-managed") {
|
||||
t.Fatalf("unexpected managed annotation")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in []string
|
||||
want map[string]struct{}
|
||||
}{
|
||||
{
|
||||
name: "empty input returns nil",
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "trims values skips blanks and deduplicates",
|
||||
in: []string{
|
||||
"alpha",
|
||||
" alpha ",
|
||||
"",
|
||||
" ",
|
||||
"beta",
|
||||
},
|
||||
want: map[string]struct{}{
|
||||
"alpha": {},
|
||||
"beta": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "case sensitive",
|
||||
in: []string{
|
||||
"alpha",
|
||||
"Alpha",
|
||||
},
|
||||
want: map[string]struct{}{
|
||||
"alpha": {},
|
||||
"Alpha": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := stringSet(tt.in...)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("expected %#v, got %#v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty input returns nil",
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "trims values and skips blanks",
|
||||
in: []string{
|
||||
"alpha",
|
||||
" alpha ",
|
||||
"",
|
||||
" ",
|
||||
"beta",
|
||||
},
|
||||
want: []string{
|
||||
"alpha",
|
||||
"alpha",
|
||||
"beta",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not deduplicate prefixes",
|
||||
in: []string{
|
||||
"alpha/",
|
||||
"alpha/",
|
||||
},
|
||||
want: []string{
|
||||
"alpha/",
|
||||
"alpha/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := compactStrings(tt.in...)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("expected %#v, got %#v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
set := map[string]struct{}{
|
||||
"existing": {},
|
||||
}
|
||||
|
||||
addStrings(
|
||||
set,
|
||||
"alpha",
|
||||
" alpha ",
|
||||
"",
|
||||
" ",
|
||||
"beta",
|
||||
)
|
||||
|
||||
want := map[string]struct{}{
|
||||
"existing": {},
|
||||
"alpha": {},
|
||||
"beta": {},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(set, want) {
|
||||
t.Fatalf("expected %#v, got %#v", want, set)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedMetadataAddMethods(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := ManagedMetadata{
|
||||
labels: map[string]struct{}{},
|
||||
annotations: map[string]struct{}{},
|
||||
}
|
||||
|
||||
m.addLabels("example.corp/label", " example.corp/trimmed-label ", "")
|
||||
m.addAnnotations("example.corp/annotation", " example.corp/trimmed-annotation ", "")
|
||||
|
||||
if !m.HasLabel("example.corp/label") {
|
||||
t.Fatalf("expected added label")
|
||||
}
|
||||
|
||||
if !m.HasLabel("example.corp/trimmed-label") {
|
||||
t.Fatalf("expected trimmed added label")
|
||||
}
|
||||
|
||||
if !m.HasAnnotation("example.corp/annotation") {
|
||||
t.Fatalf("expected added annotation")
|
||||
}
|
||||
|
||||
if !m.HasAnnotation("example.corp/trimmed-annotation") {
|
||||
t.Fatalf("expected trimmed added annotation")
|
||||
}
|
||||
|
||||
if m.HasLabel("") {
|
||||
t.Fatalf("empty label must not be added")
|
||||
}
|
||||
|
||||
if m.HasAnnotation("") {
|
||||
t.Fatalf("empty annotation must not be added")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedMetadataHasAnnotationPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := ManagedMetadata{
|
||||
annotations: map[string]struct{}{
|
||||
"example.corp/exact": {},
|
||||
},
|
||||
annotationPrefixes: []string{
|
||||
"example.corp/prefix/",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exact annotation",
|
||||
key: "example.corp/exact",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "prefix annotation",
|
||||
key: "example.corp/prefix/value",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "prefix itself also matches",
|
||||
key: "example.corp/prefix/",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "similar prefix does not match",
|
||||
key: "example.corp/prefix-other/value",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unknown annotation",
|
||||
key: "example.corp/unknown",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "case sensitive prefix",
|
||||
key: "Example.Corp/prefix/value",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
key: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := m.HasAnnotation(tt.key)
|
||||
if got != tt.want {
|
||||
t.Fatalf("expected %t, got %t", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectSkipRuleShouldSkip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rule ObjectSkipRule
|
||||
labels map[string]string
|
||||
annotations map[string]string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty rule does not skip",
|
||||
rule: ObjectSkipRule{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "matching label skips",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
},
|
||||
labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "missing label does not skip",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
},
|
||||
labels: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "non matching label value does not skip",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
},
|
||||
labels: map[string]string{
|
||||
"managed-by": "human",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "label match is case-sensitive",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
},
|
||||
labels: map[string]string{
|
||||
"managed-by": "Controller",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "matching annotation skips",
|
||||
rule: ObjectSkipRule{
|
||||
Annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "missing annotation does not skip",
|
||||
rule: ObjectSkipRule{
|
||||
Annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
},
|
||||
annotations: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "non matching annotation value does not skip",
|
||||
rule: ObjectSkipRule{
|
||||
Annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"example.corp/skip": "false",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "all labels must match",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
"owner": "capsule",
|
||||
},
|
||||
},
|
||||
labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
"owner": "other",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "all annotations must match",
|
||||
rule: ObjectSkipRule{
|
||||
Annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
"example.corp/owner": "capsule",
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
"example.corp/owner": "other",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "labels and annotations must both match",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
},
|
||||
labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matching label but missing required annotation does not skip",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
},
|
||||
labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
annotations: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "missing key must not match empty expected value",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"empty": "",
|
||||
},
|
||||
},
|
||||
labels: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "present empty value matches empty expected value",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"empty": "",
|
||||
},
|
||||
},
|
||||
labels: map[string]string{
|
||||
"empty": "",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "extra labels and annotations do not prevent match",
|
||||
rule: ObjectSkipRule{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
},
|
||||
labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
"extra": "value",
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"extra": "value",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := tt.rule.ShouldSkip(tt.labels, tt.annotations)
|
||||
if got != tt.want {
|
||||
t.Fatalf("expected %t, got %t", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectSkipRuleShouldSkipNilReceiver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var rule *ObjectSkipRule
|
||||
|
||||
if rule.ShouldSkip(
|
||||
map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
nil,
|
||||
) {
|
||||
t.Fatalf("nil rule must not skip")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultObjectSkipRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rules := DefaultObjectSkipRules()
|
||||
if len(rules) != 2 {
|
||||
t.Fatalf("expected three default skip rules, got %d", len(rules))
|
||||
}
|
||||
|
||||
if got := rules[0].Labels[NewManagedByCapsuleLabel]; got != ValueController {
|
||||
t.Fatalf("expected default skip label %q=%q, got %q", NewManagedByCapsuleLabel, ValueController, got)
|
||||
}
|
||||
|
||||
if got := rules[1].Labels[NewManagedByCapsuleLabel]; got != ValueControllerResources {
|
||||
t.Fatalf("expected legacy default skip label %q=%q, got %q", NewManagedByCapsuleLabel, ValueControllerResources, got)
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if len(rule.Annotations) != 0 {
|
||||
t.Fatalf("expected no default skip annotations, got %#v", rule.Annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkipObjectByRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
obj *metav1.PartialObjectMetadata
|
||||
rules []ObjectSkipRule
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil object does not skip",
|
||||
obj: nil,
|
||||
rules: DefaultObjectSkipRules(),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil rules do not skip",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
NewManagedByCapsuleLabel: ValueController,
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty rules do not skip",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
NewManagedByCapsuleLabel: ValueController,
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: []ObjectSkipRule{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "default Capsule controller-managed object skips",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
NewManagedByCapsuleLabel: ValueController,
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: DefaultObjectSkipRules(),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "legacy Capsule resources object skips",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
NewManagedByCapsuleLabel: ValueControllerResources,
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: DefaultObjectSkipRules(),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "default skip is case-sensitive",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
NewManagedByCapsuleLabel: "Controller",
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: DefaultObjectSkipRules(),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "plain managed-by controller is not skipped by default",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: DefaultObjectSkipRules(),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "non matching object does not skip",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
NewManagedByCapsuleLabel: "human",
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: DefaultObjectSkipRules(),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "any matching rule skips",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
"app": "demo",
|
||||
},
|
||||
map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
),
|
||||
rules: []ObjectSkipRule{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"app": "other",
|
||||
},
|
||||
},
|
||||
{
|
||||
Annotations: map[string]string{
|
||||
"example.corp/skip": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "object without labels and annotations does not skip",
|
||||
obj: objectWithMetadata(nil, nil),
|
||||
rules: []ObjectSkipRule{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty skip rule does not skip object",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
"app": "demo",
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: []ObjectSkipRule{
|
||||
{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "custom plain managed-by rule skips",
|
||||
obj: objectWithMetadata(
|
||||
map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
nil,
|
||||
),
|
||||
rules: []ObjectSkipRule{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"managed-by": "controller",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := ShouldSkipObjectByRules(tt.obj, tt.rules)
|
||||
if got != tt.want {
|
||||
t.Fatalf("expected %t, got %t", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func objectWithMetadata(
|
||||
labels map[string]string,
|
||||
annotations map[string]string,
|
||||
) *metav1.PartialObjectMetadata {
|
||||
return &metav1.PartialObjectMetadata{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: labels,
|
||||
Annotations: annotations,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ var WithoutCapsuleManagedResourcesLabelSelector = func() string {
|
||||
selection.NotIn,
|
||||
[]string{
|
||||
ValueController,
|
||||
ValueControllerResources,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -27,7 +26,6 @@ var WithCapsuleManagedResourcesLabelSelector = func() string {
|
||||
selection.In,
|
||||
[]string{
|
||||
ValueController,
|
||||
ValueControllerResources,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user