From aabf80ebbd50d5ed05a3d6ad4fe6180cd92f14d1 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 27 May 2026 22:15:00 -0500 Subject: [PATCH 1/3] feat: upgrade controller-runtime to v0.23.3 and implement client.Apply on mappedNamespaceClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrades sigs.k8s.io/controller-runtime from v0.20.4 to v0.23.3 and sigs.k8s.io/multicluster-runtime from v0.20.4-alpha.6 to v0.23.3, along with the companion k8s.io/* upgrade from v0.32.x to v0.35.0. The primary motivation is that downstream repos (compute operator) need cr v0.22+ (specifically the new client.Apply SSA method) to upgrade their NSO dependency, which in turn unblocks using LocationBinding in the workload webhook for proper location filtering. Breaking API changes addressed: - client.Client: implement Apply() on mappedNamespaceClient - multicluster-runtime: ClusterName is now a typed string (not bare string); update Provider.Get, Engage, map key types, and all test stubs - controller-runtime builder: NewWebhookManagedBy now takes the typed object as a second arg; migrate all webhooks and switch WithValidator/ WithDefaulter to the deprecated but still-working Custom variants - k8s.io/apiserver storage.Interface: Count removed, CompactRevision/Stats/ GetCurrentResourceVersion/EnableResourceSizeEstimation added; implement all new methods on projectMux and instrumentedStorage - k8s.io/apiserver: WithImpersonation moved to filters/impersonation sub-pkg - k8s.io/component-base: DefaultKubeEffectiveVersion → DefaultBuildEffectiveVersion (in apiserver/pkg/util/compatibility); NewComponentGlobalsRegistry and DefaultKubeComponent moved from featuregate to component-base/compatibility - cluster.Cluster: GetEventRecorder added to recorder.Provider; update test stubs - options.Complete: removed NamedFlagSets arg; options.Config: added ctx arg - gopkg.in/square/go-jose.v2 → gopkg.in/go-jose/go-jose.v2 - source.ForCluster: now returns (TypedSource, bool, error); discard bool Co-Authored-By: Claude Sonnet 4.6 --- cmd/milo/apiserver/config.go | 10 ++-- cmd/milo/apiserver/server.go | 8 ++-- .../controller-manager/controllermanager.go | 27 +++++++---- internal/apiserver/storage/project/mux.go | 42 +++++++++-------- .../controllers/policy/reconciler_test.go | 6 +-- .../platformaccessapproval_webhook.go | 18 +++++--- .../platformaccessarejection_webhook.go | 18 +++++--- .../v1alpha1/platforminvitation_webhook.go | 22 ++++++--- .../webhooks/iam/v1alpha1/user_webhook.go | 12 +++-- .../iam/v1alpha1/userdeactivation_webhook.go | 17 ++++--- .../iam/v1alpha1/userinvitation_webhook.go | 22 ++++++--- .../identity/v1alpha1/useridentity_webhook.go | 10 ++-- .../notes/v1alpha1/clusternote_webhook.go | 32 +++++++++---- .../webhooks/notes/v1alpha1/note_webhook.go | 32 +++++++++---- .../notification/v1alpha1/contact_webhook.go | 25 +++++++--- .../v1alpha1/contactgroup_webhook.go | 14 ++++-- .../contactgroupmembership_webhook.go | 14 ++++-- .../contactgroupmembershipremoval_webhook.go | 18 ++++++-- .../notification/v1alpha1/email_webhook.go | 17 +++++-- .../v1alpha1/emailtemplate_webhook.go | 10 ++-- .../v1alpha1/organization_webhook.go | 12 +++-- .../organizationmembership_webhook.go | 13 ++++-- .../v1alpha1/project_webhook.go | 46 +++++++++++++------ pkg/downstreamclient/mappednamespace.go | 4 ++ pkg/multicluster-runtime/milo/provider.go | 23 +++++----- .../milo/provider_test.go | 14 +++--- pkg/multicluster-runtime/source/source.go | 4 +- 27 files changed, 329 insertions(+), 161 deletions(-) diff --git a/cmd/milo/apiserver/config.go b/cmd/milo/apiserver/config.go index 1c99c9b7..b3b6c303 100644 --- a/cmd/milo/apiserver/config.go +++ b/cmd/milo/apiserver/config.go @@ -15,7 +15,7 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/endpoints/filterlatency" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" - impersonationfilters "k8s.io/apiserver/pkg/endpoints/filters/impersonation" + impersonationfilter "k8s.io/apiserver/pkg/endpoints/filters/impersonation" "k8s.io/apiserver/pkg/endpoints/request" genericfeatures "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server" @@ -26,7 +26,7 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "k8s.io/component-base/tracing" - apiservercompat "k8s.io/apiserver/pkg/util/compatibility" + utilversionscompat "k8s.io/apiserver/pkg/util/compatibility" "k8s.io/klog/v2" aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme" @@ -388,7 +388,7 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) { return nil, err } c.ControlPlane = kubeAPIs - c.ControlPlane.Generic.EffectiveVersion = apiservercompat.DefaultBuildEffectiveVersion() + c.ControlPlane.Generic.EffectiveVersion = utilversionscompat.DefaultBuildEffectiveVersion() if kubeAPIs.Generic.LoopbackClientConfig != nil && kubeAPIs.Generic.TracerProvider != nil { kubeAPIs.Generic.LoopbackClientConfig.Wrap(tracing.WrapperFor(kubeAPIs.Generic.TracerProvider)) @@ -438,7 +438,7 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) { } c.Aggregator = aggregator c.Aggregator.ExtraConfig.DisableRemoteAvailableConditionController = true - c.Aggregator.GenericConfig.EffectiveVersion = apiservercompat.DefaultBuildEffectiveVersion() + c.Aggregator.GenericConfig.EffectiveVersion = utilversionscompat.DefaultBuildEffectiveVersion() return c, nil } @@ -476,7 +476,7 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *server.Config, loopbac failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.AuditBackend, c.AuditPolicyRuleEvaluator) handler = filterlatency.TrackCompleted(handler) - handler = impersonationfilters.WithImpersonation(handler, c.Authorization.Authorizer, c.Serializer) + handler = impersonationfilter.WithImpersonation(handler, c.Authorization.Authorizer, c.Serializer) handler = filterlatency.TrackStarted(handler, c.TracerProvider, "impersonation") failedHandler = filterlatency.TrackCompleted(failedHandler) diff --git a/cmd/milo/apiserver/server.go b/cmd/milo/apiserver/server.go index c8e8f01c..ba7714cb 100644 --- a/cmd/milo/apiserver/server.go +++ b/cmd/milo/apiserver/server.go @@ -27,14 +27,14 @@ import ( "k8s.io/client-go/rest" cliflag "k8s.io/component-base/cli/flag" "k8s.io/component-base/cli/globalflag" + "k8s.io/component-base/compatibility" "k8s.io/component-base/logs" logsapi "k8s.io/component-base/logs/api/v1" _ "k8s.io/component-base/metrics/prometheus/workqueue" "k8s.io/component-base/term" "k8s.io/component-base/version" "k8s.io/component-base/version/verflag" - basecompatibility "k8s.io/component-base/compatibility" - apiservercompat "k8s.io/apiserver/pkg/util/compatibility" + utilversionscompat "k8s.io/apiserver/pkg/util/compatibility" "k8s.io/klog/v2" aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" @@ -132,9 +132,9 @@ func NewCommand() *cobra.Command { }, } - s.GenericServerRunOptions.ComponentGlobalsRegistry = basecompatibility.NewComponentGlobalsRegistry() + s.GenericServerRunOptions.ComponentGlobalsRegistry = compatibility.NewComponentGlobalsRegistry() s.GenericServerRunOptions.ComponentGlobalsRegistry.ComponentGlobalsOrRegister( - basecompatibility.DefaultKubeComponent, apiservercompat.DefaultBuildEffectiveVersion(), utilfeature.DefaultMutableFeatureGate) + compatibility.DefaultKubeComponent, utilversionscompat.DefaultBuildEffectiveVersion(), utilfeature.DefaultMutableFeatureGate) s.GenericServerRunOptions.AddUniversalFlags(namedFlagSets.FlagSet("generic")) s.Etcd.AddFlags(namedFlagSets.FlagSet("etcd")) s.SecureServing.AddFlags(namedFlagSets.FlagSet("secure serving")) diff --git a/cmd/milo/controller-manager/controllermanager.go b/cmd/milo/controller-manager/controllermanager.go index 099f1cc6..1a2157d0 100644 --- a/cmd/milo/controller-manager/controllermanager.go +++ b/cmd/milo/controller-manager/controllermanager.go @@ -43,6 +43,7 @@ import ( certutil "k8s.io/client-go/util/cert" cliflag "k8s.io/component-base/cli/flag" "k8s.io/component-base/cli/globalflag" + "k8s.io/component-base/compatibility" "k8s.io/component-base/configz" "k8s.io/component-base/featuregate" "k8s.io/component-base/logs" @@ -53,8 +54,7 @@ import ( "k8s.io/component-base/term" "k8s.io/component-base/version" "k8s.io/component-base/version/verflag" - basecompatibility "k8s.io/component-base/compatibility" - apiservercompat "k8s.io/apiserver/pkg/util/compatibility" + utilversionscompat "k8s.io/apiserver/pkg/util/compatibility" genericcontrollermanager "k8s.io/controller-manager/app" "k8s.io/controller-manager/controller" "k8s.io/controller-manager/pkg/clientbuilder" @@ -205,8 +205,8 @@ const ( // NewCommand creates a *cobra.Command object with default parameters func NewCommand() *cobra.Command { - _, _ = apiservercompat.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister( - basecompatibility.DefaultKubeComponent, apiservercompat.DefaultBuildEffectiveVersion(), utilfeature.DefaultMutableFeatureGate) + _, _ = utilversionscompat.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister( + compatibility.DefaultKubeComponent, utilversionscompat.DefaultBuildEffectiveVersion(), utilfeature.DefaultMutableFeatureGate) s, err := NewOptions() if err != nil { @@ -246,13 +246,13 @@ func NewCommand() *cobra.Command { ProjectOwnerRoleNamespace = SystemNamespace } - c, err := s.Config(cmd.Context(), KnownControllers(), nil, ControllerAliases()) + c, err := s.Config(context.Background(), KnownControllers(), nil, ControllerAliases()) if err != nil { return err } // add feature enablement metrics - fg := s.ComponentGlobalsRegistry.FeatureGateFor(basecompatibility.DefaultKubeComponent) + fg := s.ComponentGlobalsRegistry.FeatureGateFor(compatibility.DefaultKubeComponent) fg.(featuregate.MutableFeatureGate).AddMetrics() return Run(context.Background(), c.Complete(), s) }, @@ -676,8 +676,15 @@ func Run(ctx context.Context, c *config.CompletedConfig, opts *Options) error { klog.FlushAndExit(klog.ExitFlushTimeout, 1) } - // The multicluster manager automatically starts the provider because - // Provider implements multicluster.ProviderRunnable (Start method). + // Start concurrently to resolve circular dependency between provider and manager + go func() { + logger.Info("Starting Datum cluster provider") + if err := provider.Run(ctx, mcMgr); err != nil { + logger.Error(err, "Datum cluster provider failed") + panic(err) + } + }() + go func() { logger.Info("Starting multicluster manager for quota system") if err := mcMgr.Start(ctx); err != nil { @@ -831,11 +838,11 @@ func Run(ctx context.Context, c *config.CompletedConfig, opts *Options) error { } if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) { - binaryVersion, err := semver.ParseTolerant(apiservercompat.DefaultComponentGlobalsRegistry.EffectiveVersionFor(basecompatibility.DefaultKubeComponent).BinaryVersion().String()) + binaryVersion, err := semver.ParseTolerant(utilversionscompat.DefaultComponentGlobalsRegistry.EffectiveVersionFor(compatibility.DefaultKubeComponent).BinaryVersion().String()) if err != nil { return err } - emulationVersion, err := semver.ParseTolerant(apiservercompat.DefaultComponentGlobalsRegistry.EffectiveVersionFor(basecompatibility.DefaultKubeComponent).EmulationVersion().String()) + emulationVersion, err := semver.ParseTolerant(utilversionscompat.DefaultComponentGlobalsRegistry.EffectiveVersionFor(compatibility.DefaultKubeComponent).EmulationVersion().String()) if err != nil { return err } diff --git a/internal/apiserver/storage/project/mux.go b/internal/apiserver/storage/project/mux.go index 89ae23e5..e10c74a5 100644 --- a/internal/apiserver/storage/project/mux.go +++ b/internal/apiserver/storage/project/mux.go @@ -173,13 +173,7 @@ func (i *instrumentedStorage) GuaranteedUpdate(ctx context.Context, key string, i.markSuccess() return nil } -func (i *instrumentedStorage) ReadinessCheck() error { return i.inner.ReadinessCheck() } -func (i *instrumentedStorage) RequestWatchProgress(ctx context.Context) error { - if err := i.inner.RequestWatchProgress(ctx); err != nil { - return i.markReinit("watch_progress", err) - } - return nil -} +func (i *instrumentedStorage) CompactRevision() int64 { return i.inner.CompactRevision() } func (i *instrumentedStorage) Stats(ctx context.Context) (storage.Stats, error) { return i.inner.Stats(ctx) } @@ -189,7 +183,13 @@ func (i *instrumentedStorage) GetCurrentResourceVersion(ctx context.Context) (ui func (i *instrumentedStorage) EnableResourceSizeEstimation(fn storage.KeysFunc) error { return i.inner.EnableResourceSizeEstimation(fn) } -func (i *instrumentedStorage) CompactRevision() int64 { return i.inner.CompactRevision() } +func (i *instrumentedStorage) ReadinessCheck() error { return i.inner.ReadinessCheck() } +func (i *instrumentedStorage) RequestWatchProgress(ctx context.Context) error { + if err := i.inner.RequestWatchProgress(ctx); err != nil { + return i.markReinit("watch_progress", err) + } + return nil +} // -------------------- mux -------------------- @@ -374,6 +374,22 @@ func (m *projectMux) GuaranteedUpdate(ctx context.Context, key string, out runti return s.GuaranteedUpdate(ctx, key, out, ignoreNotFound, precond, tryUpdate, suggestion) } +// CompactRevision proxies to the appropriate child (defaults to the "" project). +func (m *projectMux) CompactRevision() int64 { + m.mu.RLock() + c := m.children[""] + m.mu.RUnlock() + if c == nil { + if _, err := m.childForProject(""); err != nil { + return 0 + } + m.mu.RLock() + c = m.children[""] + m.mu.RUnlock() + } + return c.s.CompactRevision() +} + // ReadinessCheck proxies to the appropriate child (defaults to the "" project). func (m *projectMux) ReadinessCheck() error { m.mu.RLock() @@ -424,13 +440,3 @@ func (m *projectMux) EnableResourceSizeEstimation(fn storage.KeysFunc) error { } return nil } - -func (m *projectMux) CompactRevision() int64 { - m.mu.RLock() - c := m.children[""] - m.mu.RUnlock() - if c == nil { - return 0 - } - return c.s.CompactRevision() -} diff --git a/internal/quota/controllers/policy/reconciler_test.go b/internal/quota/controllers/policy/reconciler_test.go index 3d89fcae..00933a5f 100644 --- a/internal/quota/controllers/policy/reconciler_test.go +++ b/internal/quota/controllers/policy/reconciler_test.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -21,7 +22,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/go-logr/logr" - "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" "sigs.k8s.io/multicluster-runtime/pkg/multicluster" @@ -50,8 +50,8 @@ func (c *testCluster) GetConfig() *rest.Config { return n func (c *testCluster) GetCache() cache.Cache { return nil } func (c *testCluster) GetFieldIndexer() client.FieldIndexer { return nil } func (c *testCluster) GetEventRecorderFor(string) record.EventRecorder { return nil } -func (c *testCluster) GetEventRecorder(string) events.EventRecorder { return nil } -func (c *testCluster) GetRESTMapper() meta.RESTMapper { return nil } +func (c *testCluster) GetEventRecorder(string) events.EventRecorder { return nil } +func (c *testCluster) GetRESTMapper() meta.RESTMapper { return nil } func (c *testCluster) GetAPIReader() client.Reader { return nil } func (c *testCluster) Start(context.Context) error { return nil } diff --git a/internal/webhooks/iam/v1alpha1/platformaccessapproval_webhook.go b/internal/webhooks/iam/v1alpha1/platformaccessapproval_webhook.go index 64b7c1da..13dd75f3 100644 --- a/internal/webhooks/iam/v1alpha1/platformaccessapproval_webhook.go +++ b/internal/webhooks/iam/v1alpha1/platformaccessapproval_webhook.go @@ -7,6 +7,7 @@ import ( "strings" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -52,10 +53,10 @@ func SetupPlatformAccessApprovalWebhooksWithManager(mgr ctrl.Manager) error { } return ctrl.NewWebhookManagedBy(mgr, &iamv1alpha1.PlatformAccessApproval{}). - WithDefaulter(&PlatformAccessApprovalMutator{ + WithCustomDefaulter(&PlatformAccessApprovalMutator{ client: mgr.GetClient(), }). - WithValidator(&PlatformAccessApprovalValidator{ + WithCustomValidator(&PlatformAccessApprovalValidator{ client: mgr.GetClient(), }). Complete() @@ -68,7 +69,11 @@ type PlatformAccessApprovalMutator struct { client client.Client } -func (m *PlatformAccessApprovalMutator) Default(ctx context.Context, paa *iamv1alpha1.PlatformAccessApproval) error { +func (m *PlatformAccessApprovalMutator) Default(ctx context.Context, obj runtime.Object) error { + paa, ok := obj.(*iamv1alpha1.PlatformAccessApproval) + if !ok { + return errors.NewInternalError(fmt.Errorf("failed to cast object to PlatformAccessApproval")) + } log := logf.FromContext(ctx).WithValues("Defaulting PlatformAccessApproval", "name", paa.GetName()) // Approver is the user who is approving the access request. @@ -111,7 +116,8 @@ type PlatformAccessApprovalValidator struct { client client.Client } -func (v *PlatformAccessApprovalValidator) ValidateCreate(ctx context.Context, paa *iamv1alpha1.PlatformAccessApproval) (admission.Warnings, error) { +func (v *PlatformAccessApprovalValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + paa := obj.(*iamv1alpha1.PlatformAccessApproval) log := logf.FromContext(ctx).WithValues("Validating PlatformAccessApproval", "name", paa.GetName()) var errs field.ErrorList @@ -186,10 +192,10 @@ func (v *PlatformAccessApprovalValidator) ValidateCreate(ctx context.Context, pa return nil, nil } -func (v *PlatformAccessApprovalValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *iamv1alpha1.PlatformAccessApproval) (admission.Warnings, error) { +func (v *PlatformAccessApprovalValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *PlatformAccessApprovalValidator) ValidateDelete(ctx context.Context, obj *iamv1alpha1.PlatformAccessApproval) (admission.Warnings, error) { +func (v *PlatformAccessApprovalValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/iam/v1alpha1/platformaccessarejection_webhook.go b/internal/webhooks/iam/v1alpha1/platformaccessarejection_webhook.go index e8a3bb9d..19d4fd88 100644 --- a/internal/webhooks/iam/v1alpha1/platformaccessarejection_webhook.go +++ b/internal/webhooks/iam/v1alpha1/platformaccessarejection_webhook.go @@ -5,6 +5,7 @@ import ( "fmt" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -18,10 +19,10 @@ const platformAccessRejectionIndexKey = "iam.miloapis.com/platformaccessrejectio func SetupPlatformAccessRejectionWebhooksWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr, &iamv1alpha1.PlatformAccessRejection{}). - WithDefaulter(&PlatformAccessRejectionMutator{ + WithCustomDefaulter(&PlatformAccessRejectionMutator{ client: mgr.GetClient(), }). - WithValidator(&PlatformAccessRejectionValidator{ + WithCustomValidator(&PlatformAccessRejectionValidator{ client: mgr.GetClient(), }). Complete() @@ -34,7 +35,11 @@ type PlatformAccessRejectionMutator struct { client client.Client } -func (m *PlatformAccessRejectionMutator) Default(ctx context.Context, par *iamv1alpha1.PlatformAccessRejection) error { +func (m *PlatformAccessRejectionMutator) Default(ctx context.Context, obj runtime.Object) error { + par, ok := obj.(*iamv1alpha1.PlatformAccessRejection) + if !ok { + return errors.NewInternalError(fmt.Errorf("failed to cast object to PlatformAccessRejection")) + } log := logf.FromContext(ctx).WithValues("Defaulting PlatformAccessRejection", "name", par.GetName()) // Rejecter is the user who is rejecting the access request. @@ -67,7 +72,8 @@ type PlatformAccessRejectionValidator struct { client client.Client } -func (v *PlatformAccessRejectionValidator) ValidateCreate(ctx context.Context, par *iamv1alpha1.PlatformAccessRejection) (admission.Warnings, error) { +func (v *PlatformAccessRejectionValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + par := obj.(*iamv1alpha1.PlatformAccessRejection) log := logf.FromContext(ctx).WithValues("Validating PlatformAccessRejection", "name", par.GetName()) var errs field.ErrorList @@ -114,10 +120,10 @@ func (v *PlatformAccessRejectionValidator) ValidateCreate(ctx context.Context, p return nil, nil } -func (v *PlatformAccessRejectionValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *iamv1alpha1.PlatformAccessRejection) (admission.Warnings, error) { +func (v *PlatformAccessRejectionValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *PlatformAccessRejectionValidator) ValidateDelete(ctx context.Context, obj *iamv1alpha1.PlatformAccessRejection) (admission.Warnings, error) { +func (v *PlatformAccessRejectionValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/iam/v1alpha1/platforminvitation_webhook.go b/internal/webhooks/iam/v1alpha1/platforminvitation_webhook.go index d3ada574..6ec35fc8 100644 --- a/internal/webhooks/iam/v1alpha1/platforminvitation_webhook.go +++ b/internal/webhooks/iam/v1alpha1/platforminvitation_webhook.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -44,8 +45,8 @@ func SetupPlatformInvitationWebhooksWithManager(mgr ctrl.Manager) error { } return ctrl.NewWebhookManagedBy(mgr, &iamv1alpha1.PlatformInvitation{}). - WithDefaulter(&PlatformInvitationMutator{client: mgr.GetClient()}). - WithValidator(&PlatformInvitationValidator{client: mgr.GetClient()}). + WithCustomDefaulter(&PlatformInvitationMutator{client: mgr.GetClient()}). + WithCustomValidator(&PlatformInvitationValidator{client: mgr.GetClient()}). Complete() } @@ -57,7 +58,12 @@ type PlatformInvitationMutator struct { } // Default sets the InvitedBy field to the requesting user. -func (m *PlatformInvitationMutator) Default(ctx context.Context, pi *iamv1alpha1.PlatformInvitation) error { +func (m *PlatformInvitationMutator) Default(ctx context.Context, obj runtime.Object) error { + pi, ok := obj.(*iamv1alpha1.PlatformInvitation) + if !ok { + return fmt.Errorf("failed to cast object to PlatformInvitation") + } + req, err := admission.RequestFromContext(ctx) if err != nil { platforminvitationlog.Error(err, "failed to get admission request from context", "name", pi.GetName()) @@ -84,7 +90,11 @@ type PlatformInvitationValidator struct { client client.Client } -func (v *PlatformInvitationValidator) ValidateCreate(ctx context.Context, pi *iamv1alpha1.PlatformInvitation) (admission.Warnings, error) { +func (v *PlatformInvitationValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + pi, ok := obj.(*iamv1alpha1.PlatformInvitation) + if !ok { + return nil, fmt.Errorf("failed to cast object to PlatformInvitation") + } platforminvitationlog.Info("Validating PlatformInvitation", "name", pi.Name) var errs field.ErrorList @@ -131,10 +141,10 @@ func (v *PlatformInvitationValidator) ValidateCreate(ctx context.Context, pi *ia return nil, nil } -func (v *PlatformInvitationValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *iamv1alpha1.PlatformInvitation) (admission.Warnings, error) { +func (v *PlatformInvitationValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *PlatformInvitationValidator) ValidateDelete(ctx context.Context, obj *iamv1alpha1.PlatformInvitation) (admission.Warnings, error) { +func (v *PlatformInvitationValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/iam/v1alpha1/user_webhook.go b/internal/webhooks/iam/v1alpha1/user_webhook.go index 13393d1d..546fcc60 100644 --- a/internal/webhooks/iam/v1alpha1/user_webhook.go +++ b/internal/webhooks/iam/v1alpha1/user_webhook.go @@ -30,7 +30,7 @@ func SetupUserWebhooksWithManager(mgr ctrl.Manager, systemNamespace string, user userlog.Info("Setting up iam.miloapis.com user webhooks") return ctrl.NewWebhookManagedBy(mgr, &iamv1alpha1.User{}). - WithValidator(&UserValidator{ + WithCustomValidator(&UserValidator{ client: mgr.GetClient(), restConfig: mgr.GetConfig(), scheme: mgr.GetScheme(), @@ -50,7 +50,8 @@ type UserValidator struct { userSelfManageRoleName string } -func (v *UserValidator) ValidateCreate(ctx context.Context, user *iamv1alpha1.User) (admission.Warnings, error) { +func (v *UserValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + user := obj.(*iamv1alpha1.User) userlog.Info("Validating User", "name", user.Name) req, err := admission.RequestFromContext(ctx) @@ -81,7 +82,10 @@ func (v *UserValidator) ValidateCreate(ctx context.Context, user *iamv1alpha1.Us return nil, nil } -func (v *UserValidator) ValidateUpdate(ctx context.Context, oldUser, newUser *iamv1alpha1.User) (admission.Warnings, error) { +func (v *UserValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + oldUser := oldObj.(*iamv1alpha1.User) + newUser := newObj.(*iamv1alpha1.User) + // If email hasn't changed, allow the update if oldUser.Spec.Email == newUser.Spec.Email { return nil, nil @@ -174,7 +178,7 @@ func (v *UserValidator) ValidateUpdate(ctx context.Context, oldUser, newUser *ia availableEmails)) } -func (v *UserValidator) ValidateDelete(ctx context.Context, obj *iamv1alpha1.User) (admission.Warnings, error) { +func (v *UserValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/iam/v1alpha1/userdeactivation_webhook.go b/internal/webhooks/iam/v1alpha1/userdeactivation_webhook.go index a2be91c2..fda0992f 100644 --- a/internal/webhooks/iam/v1alpha1/userdeactivation_webhook.go +++ b/internal/webhooks/iam/v1alpha1/userdeactivation_webhook.go @@ -22,8 +22,8 @@ func SetupUserDeactivationWebhooksWithManager(mgr ctrl.Manager, systemNamespace userdeactivationlog.Info("Setting up iam.miloapis.com userdeactivation webhooks") return ctrl.NewWebhookManagedBy(mgr, &iamv1alpha1.UserDeactivation{}). - WithDefaulter(&UserDeactivationMutator{client: mgr.GetClient(), scheme: mgr.GetScheme()}). - WithValidator(&UserDeactivationValidator{ + WithCustomDefaulter(&UserDeactivationMutator{client: mgr.GetClient(), scheme: mgr.GetScheme()}). + WithCustomValidator(&UserDeactivationValidator{ client: mgr.GetClient(), systemNamespace: systemNamespace, }). @@ -39,7 +39,11 @@ type UserDeactivationMutator struct { } // Default sets the deactivatedBy field to the username of the requesting user if it is not already set. -func (m *UserDeactivationMutator) Default(ctx context.Context, ud *iamv1alpha1.UserDeactivation) error { +func (m *UserDeactivationMutator) Default(ctx context.Context, obj runtime.Object) error { + ud, ok := obj.(*iamv1alpha1.UserDeactivation) + if !ok { + return fmt.Errorf("failed to cast object to UserDeactivation") + } userdeactivationlog.Info("Defaulting UserDeactivation", "name", ud.GetName()) req, err := admission.RequestFromContext(ctx) @@ -74,7 +78,8 @@ type UserDeactivationValidator struct { systemNamespace string } -func (v *UserDeactivationValidator) ValidateCreate(ctx context.Context, userDeactivation *iamv1alpha1.UserDeactivation) (admission.Warnings, error) { +func (v *UserDeactivationValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + userDeactivation := obj.(*iamv1alpha1.UserDeactivation) userdeactivationlog.Info("Validating UserDeactivation", "name", userDeactivation.Name) var errs field.ErrorList @@ -118,10 +123,10 @@ func (v *UserDeactivationValidator) ValidateCreate(ctx context.Context, userDeac return nil, nil } -func (v *UserDeactivationValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *iamv1alpha1.UserDeactivation) (admission.Warnings, error) { +func (v *UserDeactivationValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *UserDeactivationValidator) ValidateDelete(ctx context.Context, obj *iamv1alpha1.UserDeactivation) (admission.Warnings, error) { +func (v *UserDeactivationValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/iam/v1alpha1/userinvitation_webhook.go b/internal/webhooks/iam/v1alpha1/userinvitation_webhook.go index 65910f6e..3d4b9d69 100644 --- a/internal/webhooks/iam/v1alpha1/userinvitation_webhook.go +++ b/internal/webhooks/iam/v1alpha1/userinvitation_webhook.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -64,10 +65,10 @@ func SetupUserInvitationWebhooksWithManager(mgr ctrl.Manager, systemNamespace, a } return ctrl.NewWebhookManagedBy(mgr, &iamv1alpha1.UserInvitation{}). - WithDefaulter(&UserInvitationMutator{ + WithCustomDefaulter(&UserInvitationMutator{ client: mgr.GetClient(), }). - WithValidator(&UserInvitationValidator{ + WithCustomValidator(&UserInvitationValidator{ client: mgr.GetClient(), systemNamespace: systemNamespace, assignableRolesNamespace: assignableRolesNamespace, @@ -83,7 +84,12 @@ type UserInvitationMutator struct { } // Default sets the InvitedBy field to the requesting user if not already set. -func (m *UserInvitationMutator) Default(ctx context.Context, ui *iamv1alpha1.UserInvitation) error { +func (m *UserInvitationMutator) Default(ctx context.Context, obj runtime.Object) error { + ui, ok := obj.(*iamv1alpha1.UserInvitation) + if !ok { + return fmt.Errorf("failed to cast object to UserInvitation") + } + req, err := admission.RequestFromContext(ctx) if err != nil { userinvitationlog.Error(err, "failed to get admission request from context", "name", ui.GetName()) @@ -113,7 +119,11 @@ type UserInvitationValidator struct { } // ValidateCreate ensures the expiration date, if provided, is not already expired. -func (v *UserInvitationValidator) ValidateCreate(ctx context.Context, ui *iamv1alpha1.UserInvitation) (admission.Warnings, error) { +func (v *UserInvitationValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + ui, ok := obj.(*iamv1alpha1.UserInvitation) + if !ok { + return nil, fmt.Errorf("failed to cast object to UserInvitation") + } userinvitationlog.Info("Validating UserInvitation", "name", ui.Name) req, err := admission.RequestFromContext(ctx) @@ -189,11 +199,11 @@ func (v *UserInvitationValidator) ValidateCreate(ctx context.Context, ui *iamv1a return nil, nil } -func (v *UserInvitationValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *iamv1alpha1.UserInvitation) (admission.Warnings, error) { +func (v *UserInvitationValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *UserInvitationValidator) ValidateDelete(ctx context.Context, obj *iamv1alpha1.UserInvitation) (admission.Warnings, error) { +func (v *UserInvitationValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/identity/v1alpha1/useridentity_webhook.go b/internal/webhooks/identity/v1alpha1/useridentity_webhook.go index 2ff4e568..2d576b96 100644 --- a/internal/webhooks/identity/v1alpha1/useridentity_webhook.go +++ b/internal/webhooks/identity/v1alpha1/useridentity_webhook.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -20,22 +21,23 @@ func SetupUserIdentityWebhooksWithManager(mgr ctrl.Manager) error { useridentitylog.Info("Setting up identity.miloapis.com useridentity webhooks") return ctrl.NewWebhookManagedBy(mgr, &identityv1alpha1.UserIdentity{}). - WithValidator(&UserIdentityValidator{}). + WithCustomValidator(&UserIdentityValidator{}). Complete() } // UserIdentityValidator validates UserIdentity resources type UserIdentityValidator struct{} -func (v *UserIdentityValidator) ValidateCreate(ctx context.Context, obj *identityv1alpha1.UserIdentity) (admission.Warnings, error) { +func (v *UserIdentityValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *UserIdentityValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *identityv1alpha1.UserIdentity) (admission.Warnings, error) { +func (v *UserIdentityValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *UserIdentityValidator) ValidateDelete(ctx context.Context, userIdentity *identityv1alpha1.UserIdentity) (admission.Warnings, error) { +func (v *UserIdentityValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + userIdentity := obj.(*identityv1alpha1.UserIdentity) useridentitylog.Info("Blocking UserIdentity deletion", "name", userIdentity.Name) return nil, fmt.Errorf( diff --git a/internal/webhooks/notes/v1alpha1/clusternote_webhook.go b/internal/webhooks/notes/v1alpha1/clusternote_webhook.go index 5b4aaf2e..cf05b552 100644 --- a/internal/webhooks/notes/v1alpha1/clusternote_webhook.go +++ b/internal/webhooks/notes/v1alpha1/clusternote_webhook.go @@ -28,13 +28,13 @@ var clusterNoteLog = logf.Log.WithName("clusternote-resource") func SetupClusterNoteWebhooksWithManager(mgr ctrl.Manager, mcMgr mcmanager.Manager) error { clusterNoteLog.Info("Setting up notes.miloapis.com clusternote webhooks") return ctrl.NewWebhookManagedBy(mgr, ¬esv1alpha1.ClusterNote{}). - WithDefaulter(&ClusterNoteMutator{ + WithCustomDefaulter(&ClusterNoteMutator{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), RESTMapper: mgr.GetRESTMapper(), ClusterManager: mcMgr, }). - WithValidator(&ClusterNoteValidator{ + WithCustomValidator(&ClusterNoteValidator{ Client: mgr.GetClient(), }). Complete() @@ -49,9 +49,13 @@ type ClusterNoteMutator struct { ClusterManager mcmanager.Manager } -var _ admission.Defaulter[*notesv1alpha1.ClusterNote] = &ClusterNoteMutator{} +var _ admission.CustomDefaulter = &ClusterNoteMutator{} -func (m *ClusterNoteMutator) Default(ctx context.Context, clusterNote *notesv1alpha1.ClusterNote) error { +func (m *ClusterNoteMutator) Default(ctx context.Context, obj runtime.Object) error { + clusterNote, ok := obj.(*notesv1alpha1.ClusterNote) + if !ok { + return errors.NewInternalError(fmt.Errorf("failed to cast object to ClusterNote")) + } clusterNoteLog.Info("Defaulting ClusterNote", "name", clusterNote.Name) req, err := admission.RequestFromContext(ctx) @@ -132,15 +136,27 @@ type ClusterNoteValidator struct { Client client.Client } -var _ admission.Validator[*notesv1alpha1.ClusterNote] = &ClusterNoteValidator{} +var _ admission.CustomValidator = &ClusterNoteValidator{} -func (v *ClusterNoteValidator) ValidateCreate(ctx context.Context, clusterNote *notesv1alpha1.ClusterNote) (admission.Warnings, error) { +func (v *ClusterNoteValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + clusterNote, ok := obj.(*notesv1alpha1.ClusterNote) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to ClusterNote")) + } clusterNoteLog.Info("Validating ClusterNote creation", "name", clusterNote.Name) return nil, v.validateClusterNote(ctx, clusterNote, false) } -func (v *ClusterNoteValidator) ValidateUpdate(ctx context.Context, oldClusterNote, clusterNote *notesv1alpha1.ClusterNote) (admission.Warnings, error) { +func (v *ClusterNoteValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + clusterNote, ok := newObj.(*notesv1alpha1.ClusterNote) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to ClusterNote")) + } + oldClusterNote, ok := oldObj.(*notesv1alpha1.ClusterNote) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast old object to ClusterNote")) + } clusterNoteLog.Info("Validating ClusterNote update", "name", clusterNote.Name) skipNextActionTimeValidation := oldClusterNote.Spec.NextActionTime != nil && @@ -150,7 +166,7 @@ func (v *ClusterNoteValidator) ValidateUpdate(ctx context.Context, oldClusterNot return nil, v.validateClusterNote(ctx, clusterNote, skipNextActionTimeValidation) } -func (v *ClusterNoteValidator) ValidateDelete(ctx context.Context, obj *notesv1alpha1.ClusterNote) (admission.Warnings, error) { +func (v *ClusterNoteValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/notes/v1alpha1/note_webhook.go b/internal/webhooks/notes/v1alpha1/note_webhook.go index 8d34746b..db23e6a5 100644 --- a/internal/webhooks/notes/v1alpha1/note_webhook.go +++ b/internal/webhooks/notes/v1alpha1/note_webhook.go @@ -28,13 +28,13 @@ var noteLog = logf.Log.WithName("note-resource") func SetupNoteWebhooksWithManager(mgr ctrl.Manager, mcMgr mcmanager.Manager) error { noteLog.Info("Setting up notes.miloapis.com note webhooks") return ctrl.NewWebhookManagedBy(mgr, ¬esv1alpha1.Note{}). - WithDefaulter(&NoteMutator{ + WithCustomDefaulter(&NoteMutator{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), RESTMapper: mgr.GetRESTMapper(), ClusterManager: mcMgr, }). - WithValidator(&NoteValidator{ + WithCustomValidator(&NoteValidator{ Client: mgr.GetClient(), }). Complete() @@ -49,9 +49,13 @@ type NoteMutator struct { ClusterManager mcmanager.Manager } -var _ admission.Defaulter[*notesv1alpha1.Note] = &NoteMutator{} +var _ admission.CustomDefaulter = &NoteMutator{} -func (m *NoteMutator) Default(ctx context.Context, note *notesv1alpha1.Note) error { +func (m *NoteMutator) Default(ctx context.Context, obj runtime.Object) error { + note, ok := obj.(*notesv1alpha1.Note) + if !ok { + return errors.NewInternalError(fmt.Errorf("failed to cast object to Note")) + } noteLog.Info("Defaulting Note", "name", note.Name, "namespace", note.Namespace) req, err := admission.RequestFromContext(ctx) @@ -133,15 +137,27 @@ type NoteValidator struct { Client client.Client } -var _ admission.Validator[*notesv1alpha1.Note] = &NoteValidator{} +var _ admission.CustomValidator = &NoteValidator{} -func (v *NoteValidator) ValidateCreate(ctx context.Context, note *notesv1alpha1.Note) (admission.Warnings, error) { +func (v *NoteValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + note, ok := obj.(*notesv1alpha1.Note) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to Note")) + } noteLog.Info("Validating Note creation", "name", note.Name, "namespace", note.Namespace) return nil, v.validateNote(ctx, note, false) } -func (v *NoteValidator) ValidateUpdate(ctx context.Context, oldNote, note *notesv1alpha1.Note) (admission.Warnings, error) { +func (v *NoteValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + note, ok := newObj.(*notesv1alpha1.Note) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to Note")) + } + oldNote, ok := oldObj.(*notesv1alpha1.Note) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast old object to Note")) + } noteLog.Info("Validating Note update", "name", note.Name, "namespace", note.Namespace) skipNextActionTimeValidation := oldNote.Spec.NextActionTime != nil && @@ -151,7 +167,7 @@ func (v *NoteValidator) ValidateUpdate(ctx context.Context, oldNote, note *notes return nil, v.validateNote(ctx, note, skipNextActionTimeValidation) } -func (v *NoteValidator) ValidateDelete(ctx context.Context, obj *notesv1alpha1.Note) (admission.Warnings, error) { +func (v *NoteValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/notification/v1alpha1/contact_webhook.go b/internal/webhooks/notification/v1alpha1/contact_webhook.go index 7047de88..3618372b 100644 --- a/internal/webhooks/notification/v1alpha1/contact_webhook.go +++ b/internal/webhooks/notification/v1alpha1/contact_webhook.go @@ -62,11 +62,11 @@ func SetupContactWebhooksWithManager(mgr ctrl.Manager) error { } return ctrl.NewWebhookManagedBy(mgr, ¬ificationv1alpha1.Contact{}). - WithDefaulter(&ContactMutator{ + WithCustomDefaulter(&ContactMutator{ client: mgr.GetClient(), scheme: mgr.GetScheme(), }). - WithValidator(&ContactValidator{ + WithCustomValidator(&ContactValidator{ Client: mgr.GetClient(), }). Complete() @@ -80,7 +80,11 @@ type ContactMutator struct { scheme *runtime.Scheme } -func (m *ContactMutator) Default(ctx context.Context, contact *notificationv1alpha1.Contact) error { +func (m *ContactMutator) Default(ctx context.Context, obj runtime.Object) error { + contact, ok := obj.(*notificationv1alpha1.Contact) + if !ok { + return errors.NewInternalError(fmt.Errorf("failed to cast object to Contact")) + } contactLog.Info("Defaulting Contact", "name", contact.Name) if contact.Spec.SubjectRef != nil { @@ -106,8 +110,12 @@ type ContactValidator struct { Client client.Client } -func (v *ContactValidator) ValidateCreate(ctx context.Context, contact *notificationv1alpha1.Contact) (admission.Warnings, error) { +func (v *ContactValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { errs := field.ErrorList{} + contact, ok := obj.(*notificationv1alpha1.Contact) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to Contact")) + } contactLog.Info("Validating Contact", "name", contact.Name) // Validate Email format @@ -242,12 +250,17 @@ func contactValidationResult(errs field.ErrorList, contact *notificationv1alpha1 } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (v *ContactValidator) ValidateDelete(ctx context.Context, obj *notificationv1alpha1.Contact) (admission.Warnings, error) { +func (v *ContactValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (v *ContactValidator) ValidateUpdate(ctx context.Context, contactOld, contactNew *notificationv1alpha1.Contact) (admission.Warnings, error) { +func (v *ContactValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + contactNew, okNew := newObj.(*notificationv1alpha1.Contact) + contactOld, okOld := oldObj.(*notificationv1alpha1.Contact) + if !okNew || !okOld { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object(s) to Contact")) + } errs := field.ErrorList{} // If the SubjectRef changed, reject the update. diff --git a/internal/webhooks/notification/v1alpha1/contactgroup_webhook.go b/internal/webhooks/notification/v1alpha1/contactgroup_webhook.go index ed5d54fd..80e28f9a 100644 --- a/internal/webhooks/notification/v1alpha1/contactgroup_webhook.go +++ b/internal/webhooks/notification/v1alpha1/contactgroup_webhook.go @@ -6,6 +6,7 @@ import ( notificationv1alpha1 "go.miloapis.com/milo/pkg/apis/notification/v1alpha1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,7 +36,7 @@ func SetupContactGroupWebhooksWithManager(mgr ctrl.Manager) error { } return ctrl.NewWebhookManagedBy(mgr, ¬ificationv1alpha1.ContactGroup{}). - WithValidator(&ContactGroupValidator{ + WithCustomValidator(&ContactGroupValidator{ Client: mgr.GetClient(), }). Complete() @@ -48,7 +49,12 @@ type ContactGroupValidator struct { } // ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (v *ContactGroupValidator) ValidateCreate(ctx context.Context, cg *notificationv1alpha1.ContactGroup) (admission.Warnings, error) { +func (v *ContactGroupValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + cg, ok := obj.(*notificationv1alpha1.ContactGroup) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to ContactGroup")) + } + contactGroupLog.Info("Validating ContactGroup", "name", cg.Name) var errs field.ErrorList @@ -71,11 +77,11 @@ func (v *ContactGroupValidator) ValidateCreate(ctx context.Context, cg *notifica } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (v *ContactGroupValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *notificationv1alpha1.ContactGroup) (admission.Warnings, error) { +func (v *ContactGroupValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, nil } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (v *ContactGroupValidator) ValidateDelete(ctx context.Context, obj *notificationv1alpha1.ContactGroup) (admission.Warnings, error) { +func (v *ContactGroupValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/notification/v1alpha1/contactgroupmembership_webhook.go b/internal/webhooks/notification/v1alpha1/contactgroupmembership_webhook.go index c4b4f6f9..ffbd4680 100644 --- a/internal/webhooks/notification/v1alpha1/contactgroupmembership_webhook.go +++ b/internal/webhooks/notification/v1alpha1/contactgroupmembership_webhook.go @@ -6,6 +6,7 @@ import ( notificationv1alpha1 "go.miloapis.com/milo/pkg/apis/notification/v1alpha1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -45,7 +46,7 @@ func SetupContactGroupMembershipWebhooksWithManager(mgr ctrl.Manager) error { } return ctrl.NewWebhookManagedBy(mgr, ¬ificationv1alpha1.ContactGroupMembership{}). - WithValidator(&ContactGroupMembershipValidator{Client: mgr.GetClient()}). + WithCustomValidator(&ContactGroupMembershipValidator{Client: mgr.GetClient()}). Complete() } @@ -56,7 +57,12 @@ type ContactGroupMembershipValidator struct { } // ValidateCreate implements webhook.Validator for ContactGroupMembership -func (v *ContactGroupMembershipValidator) ValidateCreate(ctx context.Context, cgm *notificationv1alpha1.ContactGroupMembership) (admission.Warnings, error) { +func (v *ContactGroupMembershipValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + cgm, ok := obj.(*notificationv1alpha1.ContactGroupMembership) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to ContactGroupMembership")) + } + cgmLog.Info("Validating ContactGroupMembership", "name", cgm.Name) var errs field.ErrorList @@ -111,10 +117,10 @@ func (v *ContactGroupMembershipValidator) ValidateCreate(ctx context.Context, cg return nil, nil } -func (v *ContactGroupMembershipValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *notificationv1alpha1.ContactGroupMembership) (admission.Warnings, error) { +func (v *ContactGroupMembershipValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *ContactGroupMembershipValidator) ValidateDelete(ctx context.Context, obj *notificationv1alpha1.ContactGroupMembership) (admission.Warnings, error) { +func (v *ContactGroupMembershipValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/notification/v1alpha1/contactgroupmembershipremoval_webhook.go b/internal/webhooks/notification/v1alpha1/contactgroupmembershipremoval_webhook.go index 40135dcb..3ab8802c 100644 --- a/internal/webhooks/notification/v1alpha1/contactgroupmembershipremoval_webhook.go +++ b/internal/webhooks/notification/v1alpha1/contactgroupmembershipremoval_webhook.go @@ -6,6 +6,7 @@ import ( notificationv1alpha1 "go.miloapis.com/milo/pkg/apis/notification/v1alpha1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -30,7 +31,7 @@ func SetupContactGroupMembershipRemovalWebhooksWithManager(mgr ctrl.Manager) err } return ctrl.NewWebhookManagedBy(mgr, ¬ificationv1alpha1.ContactGroupMembershipRemoval{}). - WithValidator(&ContactGroupMembershipRemovalValidator{Client: mgr.GetClient()}). + WithCustomValidator(&ContactGroupMembershipRemovalValidator{Client: mgr.GetClient()}). Complete() } @@ -40,7 +41,11 @@ type ContactGroupMembershipRemovalValidator struct { Client client.Client } -func (v *ContactGroupMembershipRemovalValidator) ValidateCreate(ctx context.Context, removal *notificationv1alpha1.ContactGroupMembershipRemoval) (admission.Warnings, error) { +func (v *ContactGroupMembershipRemovalValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + removal, ok := obj.(*notificationv1alpha1.ContactGroupMembershipRemoval) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to ContactGroupMembershipRemoval")) + } var errs field.ErrorList // Ensure Contact exists @@ -82,11 +87,16 @@ func (v *ContactGroupMembershipRemovalValidator) ValidateCreate(ctx context.Cont return nil, nil } -func (v *ContactGroupMembershipRemovalValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *notificationv1alpha1.ContactGroupMembershipRemoval) (admission.Warnings, error) { +func (v *ContactGroupMembershipRemovalValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { return nil, errors.NewBadRequest("ContactGroupMembershipRemoval is immutable; delete and recreate to modify") } -func (v *ContactGroupMembershipRemovalValidator) ValidateDelete(ctx context.Context, removal *notificationv1alpha1.ContactGroupMembershipRemoval) (admission.Warnings, error) { +func (v *ContactGroupMembershipRemovalValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + removal, ok := obj.(*notificationv1alpha1.ContactGroupMembershipRemoval) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("failed to cast object to ContactGroupMembershipRemoval")) + } + // Validate contact ownership when in user context // Retrieve the referenced Contact to check its ownership contact := ¬ificationv1alpha1.Contact{} diff --git a/internal/webhooks/notification/v1alpha1/email_webhook.go b/internal/webhooks/notification/v1alpha1/email_webhook.go index 6706d91d..140052b7 100644 --- a/internal/webhooks/notification/v1alpha1/email_webhook.go +++ b/internal/webhooks/notification/v1alpha1/email_webhook.go @@ -9,6 +9,7 @@ import ( notificationv1alpha1 "go.miloapis.com/milo/pkg/apis/notification/v1alpha1" "go.miloapis.com/milo/pkg/email/templating" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -23,7 +24,7 @@ func SetupEmailWebhooksWithManager(mgr ctrl.Manager) error { emailLog.Info("Setting up notification.miloapis.com email webhooks") return ctrl.NewWebhookManagedBy(mgr, ¬ificationv1alpha1.Email{}). - WithValidator(&EmailValidator{ + WithCustomValidator(&EmailValidator{ Client: mgr.GetClient(), }). Complete() @@ -37,7 +38,11 @@ type EmailValidator struct { } // ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (v *EmailValidator) ValidateCreate(ctx context.Context, email *notificationv1alpha1.Email) (admission.Warnings, error) { +func (v *EmailValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + email, ok := obj.(*notificationv1alpha1.Email) + if !ok { + return nil, fmt.Errorf("failed to cast object to Email") + } emailLog.Info("validate create", "name", email.Name) var errs field.ErrorList @@ -105,11 +110,15 @@ func (v *EmailValidator) ValidateCreate(ctx context.Context, email *notification // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type // We do not allow updates to Email resources as they are immutable. -func (v *EmailValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *notificationv1alpha1.Email) (admission.Warnings, error) { +func (v *EmailValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + _, ok := newObj.(*notificationv1alpha1.Email) + if !ok { + return nil, fmt.Errorf("failed to cast object to Email") + } return nil, errors.NewMethodNotSupported(notificationv1alpha1.SchemeGroupVersion.WithResource("emails").GroupResource(), "update") } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (v *EmailValidator) ValidateDelete(ctx context.Context, obj *notificationv1alpha1.Email) (admission.Warnings, error) { +func (v *EmailValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/notification/v1alpha1/emailtemplate_webhook.go b/internal/webhooks/notification/v1alpha1/emailtemplate_webhook.go index 76a5c1f8..f74b3868 100644 --- a/internal/webhooks/notification/v1alpha1/emailtemplate_webhook.go +++ b/internal/webhooks/notification/v1alpha1/emailtemplate_webhook.go @@ -4,6 +4,7 @@ import ( "context" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -19,7 +20,7 @@ func SetupEmailTemplateWebhooksWithManager(mgr ctrl.Manager, _ string) error { emailTemplateLog.Info("Setting up notification.miloapis.com emailtemplates webhooks") return ctrl.NewWebhookManagedBy(mgr, ¬ificationv1alpha1.EmailTemplate{}). - WithValidator(&EmailTemplateValidator{}). + WithCustomValidator(&EmailTemplateValidator{}). Complete() } @@ -27,7 +28,8 @@ func SetupEmailTemplateWebhooksWithManager(mgr ctrl.Manager, _ string) error { type EmailTemplateValidator struct{} -func (v *EmailTemplateValidator) ValidateCreate(ctx context.Context, emailTemplate *notificationv1alpha1.EmailTemplate) (admission.Warnings, error) { +func (v *EmailTemplateValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + emailTemplate := obj.(*notificationv1alpha1.EmailTemplate) emailTemplateLog.Info("Validating EmailTemplate", "name", emailTemplate.Name) errs := field.ErrorList{} @@ -48,12 +50,12 @@ func (v *EmailTemplateValidator) ValidateCreate(ctx context.Context, emailTempla return nil, nil } -func (v *EmailTemplateValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *notificationv1alpha1.EmailTemplate) (admission.Warnings, error) { +func (v *EmailTemplateValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { // For updates we simply re-run the same validation against the new object. return v.ValidateCreate(ctx, newObj) } -func (v *EmailTemplateValidator) ValidateDelete(ctx context.Context, obj *notificationv1alpha1.EmailTemplate) (admission.Warnings, error) { +func (v *EmailTemplateValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { // No special validation on delete. return nil, nil } diff --git a/internal/webhooks/resourcemanager/v1alpha1/organization_webhook.go b/internal/webhooks/resourcemanager/v1alpha1/organization_webhook.go index f0d8f96d..791d43fe 100644 --- a/internal/webhooks/resourcemanager/v1alpha1/organization_webhook.go +++ b/internal/webhooks/resourcemanager/v1alpha1/organization_webhook.go @@ -8,6 +8,7 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -28,7 +29,7 @@ func SetupOrganizationWebhooksWithManager(mgr ctrl.Manager, systemNamespace stri organizationlog.Info("Setting up resourcemanager.miloapis.com organization webhooks") return ctrl.NewWebhookManagedBy(mgr, &resourcemanagerv1alpha1.Organization{}). - WithValidator(&OrganizationValidator{ + WithCustomValidator(&OrganizationValidator{ client: mgr.GetClient(), systemNamespace: systemNamespace, ownerRoleName: organizationOwnerRoleName, @@ -46,7 +47,8 @@ type OrganizationValidator struct { ownerRoleNamespace string } -func (v *OrganizationValidator) ValidateCreate(ctx context.Context, org *resourcemanagerv1alpha1.Organization) (admission.Warnings, error) { +func (v *OrganizationValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + org := obj.(*resourcemanagerv1alpha1.Organization) organizationlog.Info("Validating Organization", "name", org.Name) // Validate organization name length @@ -107,7 +109,9 @@ func (v *OrganizationValidator) ValidateCreate(ctx context.Context, org *resourc return nil, nil } -func (v *OrganizationValidator) ValidateUpdate(ctx context.Context, oldObj, newOrg *resourcemanagerv1alpha1.Organization) (admission.Warnings, error) { +func (v *OrganizationValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + newOrg := newObj.(*resourcemanagerv1alpha1.Organization) + // Validate organization name length if len(newOrg.Name) > 50 { return nil, apierrors.NewInvalid( @@ -126,7 +130,7 @@ func (v *OrganizationValidator) ValidateUpdate(ctx context.Context, oldObj, newO return nil, nil } -func (v *OrganizationValidator) ValidateDelete(ctx context.Context, obj *resourcemanagerv1alpha1.Organization) (admission.Warnings, error) { +func (v *OrganizationValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/internal/webhooks/resourcemanager/v1alpha1/organizationmembership_webhook.go b/internal/webhooks/resourcemanager/v1alpha1/organizationmembership_webhook.go index 309d594a..f0562f69 100644 --- a/internal/webhooks/resourcemanager/v1alpha1/organizationmembership_webhook.go +++ b/internal/webhooks/resourcemanager/v1alpha1/organizationmembership_webhook.go @@ -7,6 +7,7 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -26,7 +27,7 @@ func SetupOrganizationMembershipWebhooksWithManager(mgr ctrl.Manager, organizati organizationmembershiplog.Info("Setting up resourcemanager.miloapis.com organizationmembership webhooks") return ctrl.NewWebhookManagedBy(mgr, &resourcemanagerv1alpha1.OrganizationMembership{}). - WithValidator(&OrganizationMembershipValidator{ + WithCustomValidator(&OrganizationMembershipValidator{ client: mgr.GetClient(), apiReader: mgr.GetAPIReader(), ownerRoleName: organizationOwnerRoleName, @@ -44,7 +45,8 @@ type OrganizationMembershipValidator struct { ownerRoleNamespace string } -func (v *OrganizationMembershipValidator) ValidateCreate(ctx context.Context, membership *resourcemanagerv1alpha1.OrganizationMembership) (admission.Warnings, error) { +func (v *OrganizationMembershipValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + membership := obj.(*resourcemanagerv1alpha1.OrganizationMembership) organizationmembershiplog.Info("Validating OrganizationMembership create", "name", membership.Name, "namespace", membership.Namespace) // Validate roles if specified @@ -57,7 +59,9 @@ func (v *OrganizationMembershipValidator) ValidateCreate(ctx context.Context, me return nil, nil } -func (v *OrganizationMembershipValidator) ValidateUpdate(ctx context.Context, oldMembership, newMembership *resourcemanagerv1alpha1.OrganizationMembership) (admission.Warnings, error) { +func (v *OrganizationMembershipValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + oldMembership := oldObj.(*resourcemanagerv1alpha1.OrganizationMembership) + newMembership := newObj.(*resourcemanagerv1alpha1.OrganizationMembership) organizationmembershiplog.Info("Validating OrganizationMembership update", "name", newMembership.Name, "namespace", newMembership.Namespace) // Validate roles if specified @@ -74,7 +78,8 @@ func (v *OrganizationMembershipValidator) ValidateUpdate(ctx context.Context, ol return nil, nil } -func (v *OrganizationMembershipValidator) ValidateDelete(ctx context.Context, membership *resourcemanagerv1alpha1.OrganizationMembership) (admission.Warnings, error) { +func (v *OrganizationMembershipValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + membership := obj.(*resourcemanagerv1alpha1.OrganizationMembership) organizationmembershiplog.Info("Validating OrganizationMembership delete", "name", membership.Name, "namespace", membership.Namespace) if !v.isOwnerMembership(membership) { diff --git a/internal/webhooks/resourcemanager/v1alpha1/project_webhook.go b/internal/webhooks/resourcemanager/v1alpha1/project_webhook.go index a5460903..937dcb96 100644 --- a/internal/webhooks/resourcemanager/v1alpha1/project_webhook.go +++ b/internal/webhooks/resourcemanager/v1alpha1/project_webhook.go @@ -2,13 +2,13 @@ package v1alpha1 import ( "context" - stderrors "errors" "fmt" iamv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1" "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -24,13 +24,13 @@ func SetupProjectWebhooksWithManager(mgr ctrl.Manager, systemNamespace string, p projectlog.Info("Setting up resourcemanager.miloapis.com project webhooks") ctrl.NewWebhookManagedBy(mgr, &v1alpha1.Project{}). - WithValidator(&ProjectValidator{ + WithCustomValidator(&ProjectValidator{ Client: mgr.GetClient(), SystemNamespace: systemNamespace, ProjectOwnerRoleName: projectOwnerRoleName, ProjectOwnerRoleNamespace: projectOwnerRoleNamespace, }). - WithDefaulter(&ProjectMutator{ + WithCustomDefaulter(&ProjectMutator{ client: mgr.GetClient(), }). Complete() @@ -45,7 +45,12 @@ type ProjectMutator struct { client client.Client } -func (m *ProjectMutator) Default(ctx context.Context, project *v1alpha1.Project) error { +func (m *ProjectMutator) Default(ctx context.Context, obj runtime.Object) error { + project, ok := obj.(*v1alpha1.Project) + if !ok { + return fmt.Errorf("failed to cast object to Project") + } + req, err := admission.RequestFromContext(ctx) if err != nil { return fmt.Errorf("failed to get request from context: %w", err) @@ -59,15 +64,15 @@ func (m *ProjectMutator) Default(ctx context.Context, project *v1alpha1.Project) parentAPIGroup, parentAPIGroupOk := req.UserInfo.Extra[iamv1alpha1.ParentAPIGroupExtraKey] if !parentNameOk || !parentKindOk || !parentAPIGroupOk { - const errMsg = "request context does not have the required parent information" - projectlog.Error(stderrors.New(errMsg), errMsg) - return stderrors.New(errMsg) + errMsg := "request context does not have the required parent information" + projectlog.Error(fmt.Errorf(errMsg), errMsg) + return fmt.Errorf(errMsg) } if len(parentKind) != 1 || parentKind[0] != "Organization" || parentAPIGroup[0] != v1alpha1.GroupVersion.Group { - const errMsg = "request context has invalid parent information, must be Organization from the resourcemanager.miloapis.com API group" - projectlog.Error(stderrors.New(errMsg), errMsg) - return stderrors.New(errMsg) + errMsg := "request context has invalid parent information, must be Organization from the resourcemanager.miloapis.com API group" + projectlog.Error(fmt.Errorf(errMsg), errMsg) + return fmt.Errorf(errMsg) } requestContextOrgID := parentName[0] @@ -113,7 +118,12 @@ type ProjectValidator struct { // ValidateCreate validates the Project and creates the associated PolicyBinding // to provide the authenticated user with ownership access to the project. -func (v *ProjectValidator) ValidateCreate(ctx context.Context, project *v1alpha1.Project) (admission.Warnings, error) { +func (v *ProjectValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + project, ok := obj.(*v1alpha1.Project) + if !ok { + return nil, fmt.Errorf("failed to cast object to Project") + } + projectlog.Info("Validating Project", "name", project.Name) errs := field.ErrorList{} @@ -159,7 +169,17 @@ func (v *ProjectValidator) ValidateCreate(ctx context.Context, project *v1alpha1 return nil, nil } -func (v *ProjectValidator) ValidateUpdate(ctx context.Context, oldProject, newProject *v1alpha1.Project) (admission.Warnings, error) { +func (v *ProjectValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + oldProject, ok := oldObj.(*v1alpha1.Project) + if !ok { + return nil, fmt.Errorf("failed to cast old object to Project") + } + + newProject, ok := newObj.(*v1alpha1.Project) + if !ok { + return nil, fmt.Errorf("failed to cast new object to Project") + } + projectlog.Info("Validating Project update", "name", newProject.Name) errs := field.ErrorList{} @@ -189,7 +209,7 @@ func (v *ProjectValidator) ValidateUpdate(ctx context.Context, oldProject, newPr return nil, nil } -func (v *ProjectValidator) ValidateDelete(ctx context.Context, obj *v1alpha1.Project) (admission.Warnings, error) { +func (v *ProjectValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/pkg/downstreamclient/mappednamespace.go b/pkg/downstreamclient/mappednamespace.go index b018e393..fca7c8cb 100644 --- a/pkg/downstreamclient/mappednamespace.go +++ b/pkg/downstreamclient/mappednamespace.go @@ -279,6 +279,10 @@ func (c *mappedNamespaceClient) List(ctx context.Context, list client.ObjectList return c.client.List(ctx, list, opts...) } +func (c *mappedNamespaceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + return c.client.Apply(ctx, obj, opts...) +} + func (c *mappedNamespaceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { return c.client.Patch(ctx, obj, patch, opts...) } diff --git a/pkg/multicluster-runtime/milo/provider.go b/pkg/multicluster-runtime/milo/provider.go index ef7d802b..1e8d29a8 100644 --- a/pkg/multicluster-runtime/milo/provider.go +++ b/pkg/multicluster-runtime/milo/provider.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" "sigs.k8s.io/multicluster-runtime/pkg/multicluster" ) @@ -66,8 +67,8 @@ func New(localMgr manager.Manager, opts Options) (*Provider, error) { log: log.Log.WithName("datum-cluster-provider"), client: localMgr.GetClient(), projectRestConfig: opts.ProjectRestConfig, - projects: map[string]cluster.Cluster{}, - cancelFns: map[string]context.CancelFunc{}, + projects: map[multicluster.ClusterName]cluster.Cluster{}, + cancelFns: map[multicluster.ClusterName]context.CancelFunc{}, } if p.projectRestConfig == nil { @@ -121,9 +122,9 @@ type Provider struct { client client.Client lock sync.Mutex - mcMgr multicluster.Aware - projects map[string]cluster.Cluster - cancelFns map[string]context.CancelFunc + mcMgr mcmanager.Manager + projects map[multicluster.ClusterName]cluster.Cluster + cancelFns map[multicluster.ClusterName]context.CancelFunc indexers []index } @@ -131,19 +132,19 @@ type Provider struct { func (p *Provider) Get(_ context.Context, clusterName multicluster.ClusterName) (cluster.Cluster, error) { p.lock.Lock() defer p.lock.Unlock() - if cl, ok := p.projects[clusterName.String()]; ok { + if cl, ok := p.projects[clusterName]; ok { return cl, nil } return nil, fmt.Errorf("cluster %s not found", clusterName) } -// Start implements multicluster.ProviderRunnable and blocks until ctx is cancelled. -func (p *Provider) Start(ctx context.Context, aware multicluster.Aware) error { +// Run starts the provider and blocks. +func (p *Provider) Run(ctx context.Context, mgr mcmanager.Manager) error { p.log.Info("Starting Datum cluster provider") p.lock.Lock() - p.mcMgr = aware + p.mcMgr = mgr p.lock.Unlock() <-ctx.Done() @@ -157,7 +158,7 @@ func (p *Provider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result // Use just the project name as the key for cluster lookup. // This matches the project name used in URL paths and ParentNameExtraKey. - key := req.Name + key := multicluster.ClusterName(req.Name) var project unstructured.Unstructured if p.opts.InternalServiceDiscovery { @@ -287,7 +288,7 @@ func (p *Provider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result log.Info("Engaging cluster with multicluster manager", "key", key) // engage manager. - if err := p.mcMgr.Engage(clusterCtx, multicluster.ClusterName(key), cl); err != nil { + if err := p.mcMgr.Engage(clusterCtx, key, cl); err != nil { log.Error(err, "Failed to engage cluster with multicluster manager", "key", key) delete(p.projects, key) delete(p.cancelFns, key) diff --git a/pkg/multicluster-runtime/milo/provider_test.go b/pkg/multicluster-runtime/milo/provider_test.go index faca4430..7c18cfcb 100644 --- a/pkg/multicluster-runtime/milo/provider_test.go +++ b/pkg/multicluster-runtime/milo/provider_test.go @@ -62,7 +62,7 @@ func TestReadyProject(t *testing.T) { assert.Zero(t, result.RequeueAfter) assert.Len(t, provider.projects, 1) - cl, err := provider.Get(context.Background(), "test-project") + cl, err := provider.Get(context.Background(), multicluster.ClusterName("test-project")) assert.NoError(t, err) apiHost, err := url.Parse(cl.GetConfig().Host) assert.NoError(t, err) @@ -108,8 +108,8 @@ func TestLabelSelectorFiltering(t *testing.T) { projectRestConfig: &rest.Config{ Host: "https://localhost", }, - projects: map[string]cluster.Cluster{}, - cancelFns: map[string]context.CancelFunc{}, + projects: map[multicluster.ClusterName]cluster.Cluster{}, + cancelFns: map[multicluster.ClusterName]context.CancelFunc{}, opts: Options{ LabelSelector: labelSelector, ClusterOptions: []cluster.Option{ @@ -172,8 +172,8 @@ func TestLabelSelectorFilteringExcludesNonMatching(t *testing.T) { projectRestConfig: &rest.Config{ Host: "https://localhost", }, - projects: map[string]cluster.Cluster{}, - cancelFns: map[string]context.CancelFunc{}, + projects: map[multicluster.ClusterName]cluster.Cluster{}, + cancelFns: map[multicluster.ClusterName]context.CancelFunc{}, opts: Options{ LabelSelector: labelSelector, ClusterOptions: []cluster.Option{ @@ -228,8 +228,8 @@ func newTestProvider(projectStatus metav1.ConditionStatus, labelSelector *metav1 projectRestConfig: &rest.Config{ Host: "https://localhost", }, - projects: map[string]cluster.Cluster{}, - cancelFns: map[string]context.CancelFunc{}, + projects: map[multicluster.ClusterName]cluster.Cluster{}, + cancelFns: map[multicluster.ClusterName]context.CancelFunc{}, opts: Options{ LabelSelector: labelSelector, ClusterOptions: []cluster.Option{ diff --git a/pkg/multicluster-runtime/source/source.go b/pkg/multicluster-runtime/source/source.go index 90f304b5..9ad259dc 100644 --- a/pkg/multicluster-runtime/source/source.go +++ b/pkg/multicluster-runtime/source/source.go @@ -23,8 +23,8 @@ func NewClusterSource[object client.Object, request mcreconcile.ClusterAware[req predicates..., ) - typedSrc, _, err := src.ForCluster(multicluster.ClusterName(""), cl) - return typedSrc, err + s, _, err := src.ForCluster(multicluster.ClusterName(""), cl) + return s, err } func MustNewClusterSource[object client.Object, request mcreconcile.ClusterAware[request]]( From 5ad06a743746abe97060c2e3e89a125e87c3f82e Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 27 May 2026 22:24:31 -0500 Subject: [PATCH 2/3] fix: bump Go to 1.25, fix non-constant fmt.Errorf in project webhook k8s.io v0.35.0 requires Go >= 1.25.0. Update the Dockerfile base image from golang:1.24 to golang:1.25 and bump the go directive in go.mod from 1.23.1 to 1.25.0 so both the Docker build and the CI setup-go step (which reads go-version-file: go.mod) use the same toolchain. Also fix go vet "non-constant format string in call to fmt.Errorf" in project_webhook.go: replace fmt.Errorf(errMsg) with errors.New(errMsg) (stdlib errors) and rename the k8s.io/apimachinery/pkg/api/errors import to apierrors to avoid the collision. Co-Authored-By: Claude Sonnet 4.6 --- .../resourcemanager/v1alpha1/project_webhook.go | 17 ++++++++++------- pkg/downstreamclient/mappednamespace.go | 4 ---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/internal/webhooks/resourcemanager/v1alpha1/project_webhook.go b/internal/webhooks/resourcemanager/v1alpha1/project_webhook.go index 937dcb96..f4c48e4b 100644 --- a/internal/webhooks/resourcemanager/v1alpha1/project_webhook.go +++ b/internal/webhooks/resourcemanager/v1alpha1/project_webhook.go @@ -2,11 +2,12 @@ package v1alpha1 import ( "context" + "errors" "fmt" iamv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1" "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" @@ -65,14 +66,16 @@ func (m *ProjectMutator) Default(ctx context.Context, obj runtime.Object) error if !parentNameOk || !parentKindOk || !parentAPIGroupOk { errMsg := "request context does not have the required parent information" - projectlog.Error(fmt.Errorf(errMsg), errMsg) - return fmt.Errorf(errMsg) + err := errors.New(errMsg) + projectlog.Error(err, errMsg) + return err } if len(parentKind) != 1 || parentKind[0] != "Organization" || parentAPIGroup[0] != v1alpha1.GroupVersion.Group { errMsg := "request context has invalid parent information, must be Organization from the resourcemanager.miloapis.com API group" - projectlog.Error(fmt.Errorf(errMsg), errMsg) - return fmt.Errorf(errMsg) + err := errors.New(errMsg) + projectlog.Error(err, errMsg) + return err } requestContextOrgID := parentName[0] @@ -149,7 +152,7 @@ func (v *ProjectValidator) ValidateCreate(ctx context.Context, obj runtime.Objec } if len(errs) > 0 { - return nil, errors.NewInvalid(project.GroupVersionKind().GroupKind(), project.Name, errs) + return nil, apierrors.NewInvalid(project.GroupVersionKind().GroupKind(), project.Name, errs) } req, err := admission.RequestFromContext(ctx) @@ -203,7 +206,7 @@ func (v *ProjectValidator) ValidateUpdate(ctx context.Context, oldObj, newObj ru } if len(errs) > 0 { - return nil, errors.NewInvalid(newProject.GroupVersionKind().GroupKind(), newProject.Name, errs) + return nil, apierrors.NewInvalid(newProject.GroupVersionKind().GroupKind(), newProject.Name, errs) } return nil, nil diff --git a/pkg/downstreamclient/mappednamespace.go b/pkg/downstreamclient/mappednamespace.go index fca7c8cb..94c927c1 100644 --- a/pkg/downstreamclient/mappednamespace.go +++ b/pkg/downstreamclient/mappednamespace.go @@ -291,10 +291,6 @@ func (c *mappedNamespaceClient) Update(ctx context.Context, obj client.Object, o return c.client.Update(ctx, obj, opts...) } -func (c *mappedNamespaceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { - return c.client.Apply(ctx, obj, opts...) -} - func (c *mappedNamespaceClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { return c.client.GroupVersionKindFor(obj) } From e4258524ad426f6181f5185ae12c899cd7732e03 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 28 May 2026 14:27:36 -0500 Subject: [PATCH 3/3] fix: implement ProviderRunnable so multicluster manager auto-starts the provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Provider implemented multicluster.Provider but not multicluster.ProviderRunnable. The multicluster-runtime manager v0.23.3 only calls Start(ctx, aware) — and thus sets the Aware handle — when the provider satisfies ProviderRunnable. Without this, mcMgr (now mcAware) was never populated and every project reconcile loop would log "Multicluster manager not yet started, requeueing" indefinitely. Changes: - Replace Run(ctx, mcmanager.Manager) with Start(ctx, multicluster.Aware) - Rename mcMgr field to mcAware (typed as multicluster.Aware) - Update nil-guard and Engage call to use mcAware - Add compile-time assertion var _ multicluster.ProviderRunnable = &Provider{} - Drop now-unused mcmanager import Co-Authored-By: Claude Sonnet 4.6 --- pkg/multicluster-runtime/milo/provider.go | 26 +++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pkg/multicluster-runtime/milo/provider.go b/pkg/multicluster-runtime/milo/provider.go index 1e8d29a8..510bf777 100644 --- a/pkg/multicluster-runtime/milo/provider.go +++ b/pkg/multicluster-runtime/milo/provider.go @@ -26,7 +26,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" "sigs.k8s.io/multicluster-runtime/pkg/multicluster" ) @@ -34,6 +33,7 @@ import ( // See: https://sigs.k8s.io/multicluster-runtime/blob/7abad14c6d65fdaf9b83a2b1d9a2c99140d18e7d/providers/cluster-api/provider.go var _ multicluster.Provider = &Provider{} +var _ multicluster.ProviderRunnable = &Provider{} var projectGVK = resourcemanagerv1alpha1.GroupVersion.WithKind("Project") var projectControlPlaneGVK = infrastructurev1alpha1.GroupVersion.WithKind("ProjectControlPlane") @@ -121,9 +121,9 @@ type Provider struct { projectRestConfig *rest.Config client client.Client - lock sync.Mutex - mcMgr mcmanager.Manager - projects map[multicluster.ClusterName]cluster.Cluster + lock sync.Mutex + mcAware multicluster.Aware + projects map[multicluster.ClusterName]cluster.Cluster cancelFns map[multicluster.ClusterName]context.CancelFunc indexers []index } @@ -139,16 +139,15 @@ func (p *Provider) Get(_ context.Context, clusterName multicluster.ClusterName) return nil, fmt.Errorf("cluster %s not found", clusterName) } -// Run starts the provider and blocks. -func (p *Provider) Run(ctx context.Context, mgr mcmanager.Manager) error { +// Start implements multicluster.ProviderRunnable and is called by the +// multicluster manager to provide it with an Aware handle before controllers +// start reconciling. +func (p *Provider) Start(ctx context.Context, aware multicluster.Aware) error { p.log.Info("Starting Datum cluster provider") - p.lock.Lock() - p.mcMgr = mgr + p.mcAware = aware p.lock.Unlock() - <-ctx.Done() - return ctx.Err() } @@ -193,9 +192,8 @@ func (p *Provider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result p.lock.Lock() defer p.lock.Unlock() - // Make sure the manager has started - // TODO(jreese) what condition would lead to this? - if p.mcMgr == nil { + // Make sure the manager has started and set mcAware via Start(). + if p.mcAware == nil { log.Info("Multicluster manager not yet started, requeueing", "key", key) return ctrl.Result{RequeueAfter: time.Second * 2}, nil } @@ -288,7 +286,7 @@ func (p *Provider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result log.Info("Engaging cluster with multicluster manager", "key", key) // engage manager. - if err := p.mcMgr.Engage(clusterCtx, key, cl); err != nil { + if err := p.mcAware.Engage(clusterCtx, key, cl); err != nil { log.Error(err, "Failed to engage cluster with multicluster manager", "key", key) delete(p.projects, key) delete(p.cancelFns, key)