mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
chore: add deprecation notices (#2089)
* fix: revert pv labeling to dircet api reads Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: revert pv labeling to dircet api reads Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add deprecation warnings Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * chore: implement playground Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * chore: implement playground Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
This commit is contained in:
@@ -70,6 +70,10 @@ func (h *validationHandler) handle(
|
||||
config *capsulev1beta2.CapsuleConfiguration,
|
||||
req admission.Request,
|
||||
) *admission.Response {
|
||||
if err := validateAdmissionClients(config.Spec.Admission); err != nil {
|
||||
return ad.Deny(err.Error())
|
||||
}
|
||||
|
||||
if err := h.validateRegex(
|
||||
"spec.protectedNamespaceRegex",
|
||||
config.Spec.ProtectedNamespaceRegexpString,
|
||||
@@ -94,6 +98,22 @@ func (h *validationHandler) handle(
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAdmissionClients(config capsulev1beta2.DynamicAdmission) error {
|
||||
if config.Validating != nil {
|
||||
if err := ad.ValidateWebhookClientConfig(config.Validating.Client); err != nil {
|
||||
return fmt.Errorf("spec.admission.validating.client is invalid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Mutating != nil {
|
||||
if err := ad.ValidateWebhookClientConfig(config.Mutating.Client); err != nil {
|
||||
return fmt.Errorf("spec.admission.mutating.client is invalid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *validationHandler) validateRegex(fieldPath string, value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cfg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
runtimeadmission "github.com/projectcapsule/capsule/pkg/runtime/admission"
|
||||
)
|
||||
|
||||
func TestValidateAdmissionClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
webhookURL := "https://capsule.example.com"
|
||||
service := &admissionregistrationv1.ServiceReference{
|
||||
Name: "capsule-webhook-service",
|
||||
Namespace: "capsule-system",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config capsulev1beta2.DynamicAdmission
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "not configured"},
|
||||
{
|
||||
name: "validating url",
|
||||
config: capsulev1beta2.DynamicAdmission{
|
||||
Validating: dynamicValidatingConfig(&admissionregistrationv1.WebhookClientConfig{URL: &webhookURL}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mutating service",
|
||||
config: capsulev1beta2.DynamicAdmission{
|
||||
Mutating: dynamicMutatingConfig(&admissionregistrationv1.WebhookClientConfig{Service: service}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validating missing client",
|
||||
config: capsulev1beta2.DynamicAdmission{
|
||||
Validating: dynamicValidatingConfig(nil),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "mutating has neither",
|
||||
config: capsulev1beta2.DynamicAdmission{
|
||||
Mutating: dynamicMutatingConfig(&admissionregistrationv1.WebhookClientConfig{}),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "mutating has both",
|
||||
config: capsulev1beta2.DynamicAdmission{
|
||||
Mutating: dynamicMutatingConfig(&admissionregistrationv1.WebhookClientConfig{
|
||||
URL: &webhookURL,
|
||||
Service: service,
|
||||
}),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateAdmissionClients(test.config)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("validateAdmissionClients() error = %v, wantErr %t", err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func dynamicValidatingConfig(
|
||||
client *admissionregistrationv1.WebhookClientConfig,
|
||||
) *capsulev1beta2.DynamicValidatingAdmissionConfig {
|
||||
return &capsulev1beta2.DynamicValidatingAdmissionConfig{
|
||||
DynamicAdmissionConfig: runtimeadmission.DynamicAdmissionConfig{Client: client},
|
||||
}
|
||||
}
|
||||
|
||||
func dynamicMutatingConfig(
|
||||
client *admissionregistrationv1.WebhookClientConfig,
|
||||
) *capsulev1beta2.DynamicMutatingAdmissionConfig {
|
||||
return &capsulev1beta2.DynamicMutatingAdmissionConfig{
|
||||
DynamicAdmissionConfig: runtimeadmission.DynamicAdmissionConfig{Client: client},
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ func RequiredMetadataHandler() handlers.TypedHandlerWithTenantUser[*corev1.Names
|
||||
return &requiredMetadataHandler{}
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func (h *requiredMetadataHandler) OnCreate(
|
||||
_ client.Client,
|
||||
_ client.Reader,
|
||||
@@ -61,6 +62,7 @@ func (h *requiredMetadataHandler) OnCreate(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func (h *requiredMetadataHandler) OnUpdate(
|
||||
_ client.Client,
|
||||
_ client.Reader,
|
||||
|
||||
@@ -149,6 +149,7 @@ func (h *userMetadataHandler) OnDelete(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func validateUserMetadata(
|
||||
ctx context.Context,
|
||||
req admission.Request,
|
||||
|
||||
@@ -72,6 +72,7 @@ func (h *validating) OnDelete(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func (h *validating) handle(
|
||||
ctx context.Context,
|
||||
req admission.Request,
|
||||
|
||||
@@ -67,6 +67,7 @@ func (h *forbiddenAnnotationsRegexHandler) OnUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func (h *forbiddenAnnotationsRegexHandler) validate(tnt *capsulev1beta2.Tenant, req admission.Request) *admission.Response {
|
||||
if tnt == nil || tnt.Spec.NamespaceOptions == nil {
|
||||
return nil
|
||||
|
||||
@@ -63,6 +63,7 @@ func (h *namespaceMetadataHandler) OnUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func validateTenantNamespaceMetadata(tnt *capsulev1beta2.Tenant) *admission.Response {
|
||||
if tnt == nil {
|
||||
return nil
|
||||
|
||||
@@ -72,6 +72,7 @@ func (h *requiredMetadataHandler) OnUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func (h *requiredMetadataHandler) validate(tnt *capsulev1beta2.Tenant, req admission.Request) *admission.Response {
|
||||
no := tnt.Spec.NamespaceOptions
|
||||
if no == nil || no.RequiredMetadata == nil {
|
||||
@@ -118,6 +119,7 @@ func requiredMetadataChanged(oldT, newT *capsulev1beta2.Tenant) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func getRequiredMetadata(t *capsulev1beta2.Tenant) *capsulev1beta2.RequiredMetadata {
|
||||
// Adjust the return type to your actual struct type:
|
||||
// e.g. *capsulev1beta2.NamespaceRequiredMetadata or similar.
|
||||
|
||||
@@ -82,6 +82,8 @@ func (h *warningHandler) handle(tnt *capsulev1beta2.Tenant, req admission.Reques
|
||||
}
|
||||
}
|
||||
|
||||
response.Warnings = append(response.Warnings, deprecatedTenantFieldWarnings(tnt)...)
|
||||
|
||||
//nolint:staticcheck
|
||||
if len(tnt.Spec.LimitRanges.Items) > 0 {
|
||||
response.Warnings = append(response.Warnings,
|
||||
@@ -96,12 +98,7 @@ func (h *warningHandler) handle(tnt *capsulev1beta2.Tenant, req admission.Reques
|
||||
)
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
|
||||
response.Warnings = append(response.Warnings,
|
||||
"The field `additionalMetadata` is deprecated and will be removed in a future release. Please migrate to `additionalMetadataList`. See: https://projectcapsule.dev/docs/tenants/metadata/#additionalmetadatalist.",
|
||||
)
|
||||
}
|
||||
response.Warnings = append(response.Warnings, deprecatedNamespaceOptionWarnings(tnt)...)
|
||||
|
||||
//nolint:staticcheck
|
||||
if tnt.Spec.StorageClasses != nil && tnt.Spec.StorageClasses.Regex != "" {
|
||||
@@ -143,3 +140,73 @@ func (h *warningHandler) handle(tnt *capsulev1beta2.Tenant, req admission.Reques
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func deprecatedTenantFieldWarnings(tnt *capsulev1beta2.Tenant) (warnings []string) {
|
||||
//nolint:staticcheck
|
||||
if tnt.Spec.ResourceQuota.Scope != "" || len(tnt.Spec.ResourceQuota.Items) > 0 {
|
||||
warnings = append(warnings,
|
||||
"The field `resourceQuotas` is deprecated and will be removed in a future release. Please migrate to rules quotas. See: https://projectcapsule.dev/docs/tenants/rules/#quotas.",
|
||||
)
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
if tnt.Spec.ServiceOptions != nil {
|
||||
warnings = append(warnings,
|
||||
"The field `serviceOptions` is deprecated and will be removed in a future release. Please migrate to rules metadata. See: https://projectcapsule.dev/docs/rules/enforcement/metadata/.",
|
||||
)
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
if tnt.Spec.PodOptions != nil {
|
||||
warnings = append(warnings,
|
||||
"The field `podOptions` is deprecated and will be removed in a future release. Please migrate to rules metadata. See: https://projectcapsule.dev/docs/rules/enforcement/metadata/.",
|
||||
)
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
func deprecatedNamespaceOptionWarnings(tnt *capsulev1beta2.Tenant) (warnings []string) {
|
||||
if tnt.Spec.NamespaceOptions == nil {
|
||||
return warnings
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
if tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
|
||||
warnings = append(warnings,
|
||||
"The field `additionalMetadata` is deprecated and will be removed in a future release. Please migrate to rules metadata. See: https://projectcapsule.dev/docs/rules/enforcement/metadata/#namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
if len(tnt.Spec.NamespaceOptions.AdditionalMetadataList) > 0 {
|
||||
warnings = append(warnings,
|
||||
"The field `additionalMetadataList` is deprecated and will be removed in a future release. Please migrate to rules metadata. See: https://projectcapsule.dev/docs/rules/enforcement/metadata/#namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
if tnt.Spec.NamespaceOptions.RequiredMetadata != nil {
|
||||
warnings = append(warnings,
|
||||
"The field `requiredMetadata` is deprecated and will be removed in a future release. Please migrate to rules metadata. See: https://projectcapsule.dev/docs/rules/enforcement/metadata/#namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
if len(tnt.Spec.NamespaceOptions.ForbiddenLabels.Exact) > 0 ||
|
||||
tnt.Spec.NamespaceOptions.ForbiddenLabels.Regex != "" {
|
||||
warnings = append(warnings,
|
||||
"The field `forbiddenLabels` is deprecated and will be removed in a future release. Please migrate to rules metadata. See: https://projectcapsule.dev/docs/rules/enforcement/metadata/#namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
if len(tnt.Spec.NamespaceOptions.ForbiddenAnnotations.Exact) > 0 ||
|
||||
tnt.Spec.NamespaceOptions.ForbiddenAnnotations.Regex != "" {
|
||||
warnings = append(warnings,
|
||||
"The field `forbiddenAnnotations` is deprecated and will be removed in a future release. Please migrate to rules metadata. See: https://projectcapsule.dev/docs/rules/enforcement/metadata/#namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
|
||||
//nolint:staticcheck
|
||||
func TestDeprecatedTenantFieldsWarnings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configure func(*capsulev1beta2.Tenant)
|
||||
field string
|
||||
}{
|
||||
{
|
||||
name: "resource quota items",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.ResourceQuota.Items = []corev1.ResourceQuotaSpec{{}}
|
||||
},
|
||||
field: "`resourceQuotas`",
|
||||
},
|
||||
{
|
||||
name: "resource quota scope",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.ResourceQuota.Scope = api.ResourceQuotaScopeTenant
|
||||
},
|
||||
field: "`resourceQuotas`",
|
||||
},
|
||||
{
|
||||
name: "service options",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.ServiceOptions = &api.ServiceOptions{}
|
||||
},
|
||||
field: "`serviceOptions`",
|
||||
},
|
||||
{
|
||||
name: "pod options",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.PodOptions = &api.PodOptions{}
|
||||
},
|
||||
field: "`podOptions`",
|
||||
},
|
||||
{
|
||||
name: "additional metadata list",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{{}},
|
||||
}
|
||||
},
|
||||
field: "`additionalMetadataList`",
|
||||
},
|
||||
{
|
||||
name: "required metadata",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
RequiredMetadata: &capsulev1beta2.RequiredMetadata{},
|
||||
}
|
||||
},
|
||||
field: "`requiredMetadata`",
|
||||
},
|
||||
{
|
||||
name: "forbidden labels exact",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ForbiddenLabels: api.ForbiddenListSpec{Exact: []string{"blocked"}},
|
||||
}
|
||||
},
|
||||
field: "`forbiddenLabels`",
|
||||
},
|
||||
{
|
||||
name: "forbidden labels regex",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ForbiddenLabels: api.ForbiddenListSpec{Regex: "blocked-.*"},
|
||||
}
|
||||
},
|
||||
field: "`forbiddenLabels`",
|
||||
},
|
||||
{
|
||||
name: "forbidden annotations exact",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ForbiddenAnnotations: api.ForbiddenListSpec{Exact: []string{"blocked"}},
|
||||
}
|
||||
},
|
||||
field: "`forbiddenAnnotations`",
|
||||
},
|
||||
{
|
||||
name: "forbidden annotations regex",
|
||||
configure: func(tnt *capsulev1beta2.Tenant) {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ForbiddenAnnotations: api.ForbiddenListSpec{Regex: "blocked-.*"},
|
||||
}
|
||||
},
|
||||
field: "`forbiddenAnnotations`",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{}
|
||||
tt.configure(tnt)
|
||||
|
||||
response := (&warningHandler{}).handle(tnt, admission.Request{})
|
||||
if len(response.Warnings) != 1 {
|
||||
t.Fatalf("warnings = %v, want exactly one warning", response.Warnings)
|
||||
}
|
||||
|
||||
if !strings.Contains(response.Warnings[0], tt.field) {
|
||||
t.Fatalf("warning = %q, want field %s", response.Warnings[0], tt.field)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeprecatedTenantFieldsWarningsAreAbsentForZeroValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
Spec: capsulev1beta2.TenantSpec{
|
||||
NamespaceOptions: &capsulev1beta2.NamespaceOptions{},
|
||||
},
|
||||
}
|
||||
|
||||
response := (&warningHandler{}).handle(tnt, admission.Request{})
|
||||
if len(response.Warnings) != 0 {
|
||||
t.Fatalf("warnings = %v, want no warnings", response.Warnings)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user