diff --git a/cmd/resource-generator/data/adapter.template b/cmd/resource-generator/data/adapter.template index d59c8cca..4f87c3e0 100644 --- a/cmd/resource-generator/data/adapter.template +++ b/cmd/resource-generator/data/adapter.template @@ -23,9 +23,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.{{ .Name }} - resourceSpecT = orcv1alpha1.{{ .Name }}ResourceSpec - filterT = orcv1alpha1.{{ .Name }}Filter + orcObjectT = orcv1alpha1.{{ .Name }} + orcObjectListT = orcv1alpha1.{{ .Name }}List + resourceSpecT = orcv1alpha1.{{ .Name }}ResourceSpec + filterT = orcv1alpha1.{{ .Name }}Filter ) // Derived types diff --git a/cmd/resource-generator/data/controller.template b/cmd/resource-generator/data/controller.template new file mode 100644 index 00000000..f98f70bf --- /dev/null +++ b/cmd/resource-generator/data/controller.template @@ -0,0 +1,44 @@ +/* +Copyright {{ .Year }} The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package {{ .NameLower }} + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/cmd/resource-generator/main.go b/cmd/resource-generator/main.go index a22afb12..0b5f67da 100644 --- a/cmd/resource-generator/main.go +++ b/cmd/resource-generator/main.go @@ -20,6 +20,9 @@ var api_template string //go:embed data/adapter.template var adapter_template string +//go:embed data/controller.template +var controller_template string + type specExtraValidation struct { Rule string Message string @@ -92,6 +95,7 @@ var allResources []templateFields = []templateFields{ func main() { apiTemplate := template.Must(template.New("api").Parse(api_template)) adapterTemplate := template.Must(template.New("adapter").Parse(adapter_template)) + controllerTemplate := template.Must(template.New("controller").Parse(controller_template)) for i := range allResources { resource := &allResources[i] @@ -112,8 +116,13 @@ func main() { panic(err) } - facadePath := filepath.Join("internal", "controllers", resourceLower, "zz_generated.adapter.go") - if err := writeTemplate(facadePath, adapterTemplate, resource); err != nil { + adapterPath := filepath.Join("internal", "controllers", resourceLower, "zz_generated.adapter.go") + if err := writeTemplate(adapterPath, adapterTemplate, resource); err != nil { + panic(err) + } + + controllerPath := filepath.Join("internal", "controllers", resourceLower, "zz_generated.controller.go") + if err := writeTemplate(controllerPath, controllerTemplate, resource); err != nil { panic(err) } } diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 262c94c7..3148e5cb 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -11,6 +11,8 @@ rules: verbs: - get - list + - patch + - update - watch - apiGroups: - openstack.k-orc.cloud diff --git a/internal/controllers/flavor/actuator.go b/internal/controllers/flavor/actuator.go index 59e4eadf..d4cd8176 100644 --- a/internal/controllers/flavor/actuator.go +++ b/internal/controllers/flavor/actuator.go @@ -21,6 +21,7 @@ import ( "iter" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" @@ -167,21 +168,27 @@ type flavorHelperFactory struct{} var _ helperFactory = flavorHelperFactory{} -func newActuator(ctx context.Context, orcObject *orcv1alpha1.Flavor, controller generic.ResourceController) (flavorActuator, error) { +func newActuator(ctx context.Context, orcObject *orcv1alpha1.Flavor, controller generic.ResourceController) (flavorActuator, []generic.ProgressStatus, error) { log := ctrl.LoggerFrom(ctx) + // Ensure credential secrets exist and have our finalizer + _, progressStatus, err := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if len(progressStatus) > 0 || err != nil { + return flavorActuator{}, progressStatus, err + } + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) if err != nil { - return flavorActuator{}, err + return flavorActuator{}, nil, err } osClient, err := clientScope.NewComputeClient() if err != nil { - return flavorActuator{}, err + return flavorActuator{}, nil, err } return flavorActuator{ osClient: osClient, - }, nil + }, nil, nil } func (flavorHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { @@ -189,11 +196,11 @@ func (flavorHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { } func (flavorHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, createResourceActuator, error) { - actuator, err := newActuator(ctx, orcObject, controller) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } func (flavorHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, deleteResourceActuator, error) { - actuator, err := newActuator(ctx, orcObject, controller) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } diff --git a/internal/controllers/flavor/controller.go b/internal/controllers/flavor/controller.go index 005c35c8..16f9ecee 100644 --- a/internal/controllers/flavor/controller.go +++ b/internal/controllers/flavor/controller.go @@ -18,6 +18,7 @@ package flavor import ( "context" + "errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -27,8 +28,11 @@ import ( ctrlexport "github.com/k-orc/openstack-resource-controller/internal/controllers/export" "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/scope" + "github.com/k-orc/openstack-resource-controller/internal/util/credentials" ) +const controllerName = "flavor" + // +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=flavors,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=flavors/status,verbs=get;update;patch @@ -41,15 +45,24 @@ func New(scopeFactory scope.Factory) ctrlexport.Controller { } func (flavorReconcilerConstructor) GetName() string { - return "flavor" + return controllerName } // SetupWithManager sets up the controller with the Manager. func (c flavorReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { - reconciler := generic.NewController(c.GetName(), mgr.GetClient(), c.scopeFactory, flavorHelperFactory{}, flavorStatusWriter{}) + log := ctrl.LoggerFrom(ctx) - return ctrl.NewControllerManagedBy(mgr). - For(&orcv1alpha1.Flavor{}). + builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). - Complete(&reconciler) + For(&orcv1alpha1.Flavor{}) + + if err := errors.Join( + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), + ); err != nil { + return err + } + + reconciler := generic.NewController(controllerName, mgr.GetClient(), c.scopeFactory, flavorHelperFactory{}, flavorStatusWriter{}) + return builder.Complete(&reconciler) } diff --git a/internal/controllers/flavor/zz_generated.adapter.go b/internal/controllers/flavor/zz_generated.adapter.go index 88560f78..52c9b963 100644 --- a/internal/controllers/flavor/zz_generated.adapter.go +++ b/internal/controllers/flavor/zz_generated.adapter.go @@ -24,9 +24,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.Flavor - resourceSpecT = orcv1alpha1.FlavorResourceSpec - filterT = orcv1alpha1.FlavorFilter + orcObjectT = orcv1alpha1.Flavor + orcObjectListT = orcv1alpha1.FlavorList + resourceSpecT = orcv1alpha1.FlavorResourceSpec + filterT = orcv1alpha1.FlavorFilter ) // Derived types diff --git a/internal/controllers/flavor/zz_generated.controller.go b/internal/controllers/flavor/zz_generated.controller.go new file mode 100644 index 00000000..27490ac0 --- /dev/null +++ b/internal/controllers/flavor/zz_generated.controller.go @@ -0,0 +1,45 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flavor + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/internal/controllers/image/actuator.go b/internal/controllers/image/actuator.go index 0d1fdad3..56bc905b 100644 --- a/internal/controllers/image/actuator.go +++ b/internal/controllers/image/actuator.go @@ -24,6 +24,7 @@ import ( "slices" "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" @@ -45,21 +46,27 @@ type imageActuator struct { osClient osclients.ImageClient } -func newActuator(ctx context.Context, controller generic.ResourceController, orcObject *orcv1alpha1.Image) (imageActuator, error) { +func newActuator(ctx context.Context, controller generic.ResourceController, orcObject *orcv1alpha1.Image) (imageActuator, []generic.ProgressStatus, error) { log := ctrl.LoggerFrom(ctx) + // Ensure credential secrets exist and have our finalizer + _, progressStatus, err := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if len(progressStatus) > 0 || err != nil { + return imageActuator{}, progressStatus, err + } + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) if err != nil { - return imageActuator{}, err + return imageActuator{}, nil, err } osClient, err := clientScope.NewImageClient() if err != nil { - return imageActuator{}, err + return imageActuator{}, nil, err } return imageActuator{ osClient: osClient, - }, nil + }, nil, nil } var _ createResourceActuator = imageActuator{} diff --git a/internal/controllers/image/controller.go b/internal/controllers/image/controller.go index b64ce5bb..49f2258e 100644 --- a/internal/controllers/image/controller.go +++ b/internal/controllers/image/controller.go @@ -18,6 +18,7 @@ package image import ( "context" + "errors" "time" "k8s.io/client-go/tools/record" @@ -30,12 +31,15 @@ import ( ctrlexport "github.com/k-orc/openstack-resource-controller/internal/controllers/export" "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/scope" + "github.com/k-orc/openstack-resource-controller/internal/util/credentials" ) const ( FieldOwner = "openstack.k-orc.cloud/imagecontroller" // Field owner of transient status. SSAStatusTxn = "status" + + controllerName = "image" ) // ssaFieldOwner returns the field owner for a specific named SSA transaction. @@ -63,7 +67,7 @@ func New(scopeFactory scope.Factory) ctrlexport.Controller { } func (imageReconcilerConstructor) GetName() string { - return "image" + return controllerName } // orcImageReconciler reconciles an ORC Image. @@ -85,16 +89,25 @@ func (r *orcImageReconciler) GetScopeFactory() scope.Factory { } // SetupWithManager sets up the controller with the Manager. -func (c imageReconcilerConstructor) SetupWithManager(_ context.Context, mgr ctrl.Manager, options controller.Options) error { +func (c imageReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + log := ctrl.LoggerFrom(ctx) + + builder := ctrl.NewControllerManagedBy(mgr). + WithOptions(options). + For(&orcv1alpha1.Image{}) + + if err := errors.Join( + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), + ); err != nil { + return err + } + reconciler := orcImageReconciler{ client: mgr.GetClient(), recorder: mgr.GetEventRecorderFor("orc-image-controller"), imageReconcilerConstructor: c, } - - return ctrl.NewControllerManagedBy(mgr). - For(&orcv1alpha1.Image{}). - WithOptions(options). - Complete(&reconciler) + return builder.Complete(&reconciler) } diff --git a/internal/controllers/image/reconcile.go b/internal/controllers/image/reconcile.go index 1761187f..dfa3fcf0 100644 --- a/internal/controllers/image/reconcile.go +++ b/internal/controllers/image/reconcile.go @@ -76,8 +76,9 @@ func (r *orcImageReconciler) reconcileNormal(ctx context.Context, orcObject *orc } }() - actuator, err := newActuator(ctx, r, orcObject) - if err != nil { + actuator, progressStatus, err := newActuator(ctx, r, orcObject) + if len(progressStatus) > 0 || err != nil { + addStatus(withProgressStatus(progressStatus...)) return ctrl.Result{}, err } @@ -197,8 +198,9 @@ func (r *orcImageReconciler) reconcileDelete(ctx context.Context, orcObject *orc } }() - actuator, err := newActuator(ctx, r, orcObject) - if err != nil { + actuator, progressStatus, err := newActuator(ctx, r, orcObject) + if len(progressStatus) > 0 || err != nil { + addStatus(withProgressStatus(progressStatus...)) return ctrl.Result{}, err } diff --git a/internal/controllers/image/status.go b/internal/controllers/image/status.go index f9bad41e..84003711 100644 --- a/internal/controllers/image/status.go +++ b/internal/controllers/image/status.go @@ -32,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/util/applyconfigs" orcerrors "github.com/k-orc/openstack-resource-controller/internal/util/errors" orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/pkg/clients/applyconfiguration/api/v1alpha1" @@ -57,6 +58,7 @@ type updateStatusOpts struct { progressMessage *string err error incrementDownloadAttempts bool + progressStatus []generic.ProgressStatus } type updateStatusOpt func(*updateStatusOpts) @@ -73,6 +75,12 @@ func withError(err error) updateStatusOpt { } } +func withProgressStatus(progressStatus ...generic.ProgressStatus) updateStatusOpt { + return func(opts *updateStatusOpts) { + opts.progressStatus = append(opts.progressStatus, progressStatus...) + } +} + // withProgressMessage sets a custom progressing message if and only if the reconcile is progressing. func withProgressMessage(message string) updateStatusOpt { return func(opts *updateStatusOpts) { @@ -171,11 +179,16 @@ func createStatusUpdate(ctx context.Context, orcImage *orcv1alpha1.Image, now me WithStatus(metav1.ConditionTrue). WithReason(orcv1alpha1.ConditionReasonProgressing) - if statusOpts.progressMessage == nil { - progressingCondition.WithMessage("Reconciliation is progressing") + progressMessage := "" + if len(statusOpts.progressStatus) > 0 { + progressMessage = statusOpts.progressStatus[0].Message() + } else if statusOpts.progressMessage != nil { + progressMessage = *statusOpts.progressMessage } else { - progressingCondition.WithMessage(*statusOpts.progressMessage) + progressMessage = "Reconciliation is progressing" } + + progressingCondition.WithMessage(progressMessage) } } else { progressingCondition.WithStatus(metav1.ConditionFalse) diff --git a/internal/controllers/image/zz_generated.adapter.go b/internal/controllers/image/zz_generated.adapter.go index bc04c491..c9ffc2b3 100644 --- a/internal/controllers/image/zz_generated.adapter.go +++ b/internal/controllers/image/zz_generated.adapter.go @@ -24,9 +24,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.Image - resourceSpecT = orcv1alpha1.ImageResourceSpec - filterT = orcv1alpha1.ImageFilter + orcObjectT = orcv1alpha1.Image + orcObjectListT = orcv1alpha1.ImageList + resourceSpecT = orcv1alpha1.ImageResourceSpec + filterT = orcv1alpha1.ImageFilter ) // Derived types diff --git a/internal/controllers/image/zz_generated.controller.go b/internal/controllers/image/zz_generated.controller.go new file mode 100644 index 00000000..10d8463a --- /dev/null +++ b/internal/controllers/image/zz_generated.controller.go @@ -0,0 +1,45 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package image + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/internal/controllers/network/actuator.go b/internal/controllers/network/actuator.go index 85dc3b0b..f794c9c2 100644 --- a/internal/controllers/network/actuator.go +++ b/internal/controllers/network/actuator.go @@ -25,6 +25,7 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/mtu" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/portsecurity" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" @@ -162,21 +163,27 @@ type networkHelperFactory struct{} var _ helperFactory = networkHelperFactory{} -func newActuator(ctx context.Context, orcObject *orcv1alpha1.Network, controller generic.ResourceController) (networkActuator, error) { +func newActuator(ctx context.Context, orcObject *orcv1alpha1.Network, controller generic.ResourceController) (networkActuator, []generic.ProgressStatus, error) { log := ctrl.LoggerFrom(ctx) + // Ensure credential secrets exist and have our finalizer + _, progressStatus, err := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if len(progressStatus) > 0 || err != nil { + return networkActuator{}, progressStatus, err + } + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) if err != nil { - return networkActuator{}, err + return networkActuator{}, nil, err } osClient, err := clientScope.NewNetworkClient() if err != nil { - return networkActuator{}, err + return networkActuator{}, nil, err } return networkActuator{ osClient: osClient, - }, nil + }, nil, nil } func (networkHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { @@ -184,11 +191,11 @@ func (networkHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { } func (networkHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, createResourceActuator, error) { - actuator, err := newActuator(ctx, orcObject, controller) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } func (networkHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, deleteResourceActuator, error) { - actuator, err := newActuator(ctx, orcObject, controller) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } diff --git a/internal/controllers/network/controller.go b/internal/controllers/network/controller.go index 56df2e5e..854c459d 100644 --- a/internal/controllers/network/controller.go +++ b/internal/controllers/network/controller.go @@ -18,6 +18,7 @@ package network import ( "context" + "errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -27,8 +28,11 @@ import ( ctrlexport "github.com/k-orc/openstack-resource-controller/internal/controllers/export" "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/scope" + "github.com/k-orc/openstack-resource-controller/internal/util/credentials" ) +const controllerName = "network" + // +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=networks,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=networks/status,verbs=get;update;patch @@ -43,15 +47,24 @@ func New(scopeFactory scope.Factory) ctrlexport.Controller { } func (networkReconcilerConstructor) GetName() string { - return "network" + return controllerName } // SetupWithManager sets up the controller with the Manager. -func (c networkReconcilerConstructor) SetupWithManager(_ context.Context, mgr ctrl.Manager, options controller.Options) error { - reconciler := generic.NewController(c.GetName(), mgr.GetClient(), c.scopeFactory, networkHelperFactory{}, networkStatusWriter{}) +func (c networkReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + log := ctrl.LoggerFrom(ctx) - return ctrl.NewControllerManagedBy(mgr). - For(&orcv1alpha1.Network{}). + builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). - Complete(&reconciler) + For(&orcv1alpha1.Network{}) + + if err := errors.Join( + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), + ); err != nil { + return err + } + + reconciler := generic.NewController(controllerName, mgr.GetClient(), c.scopeFactory, networkHelperFactory{}, networkStatusWriter{}) + return builder.Complete(&reconciler) } diff --git a/internal/controllers/network/zz_generated.adapter.go b/internal/controllers/network/zz_generated.adapter.go index 621abc64..63ee3be1 100644 --- a/internal/controllers/network/zz_generated.adapter.go +++ b/internal/controllers/network/zz_generated.adapter.go @@ -24,9 +24,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.Network - resourceSpecT = orcv1alpha1.NetworkResourceSpec - filterT = orcv1alpha1.NetworkFilter + orcObjectT = orcv1alpha1.Network + orcObjectListT = orcv1alpha1.NetworkList + resourceSpecT = orcv1alpha1.NetworkResourceSpec + filterT = orcv1alpha1.NetworkFilter ) // Derived types diff --git a/internal/controllers/network/zz_generated.controller.go b/internal/controllers/network/zz_generated.controller.go new file mode 100644 index 00000000..3135415f --- /dev/null +++ b/internal/controllers/network/zz_generated.controller.go @@ -0,0 +1,45 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package network + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/internal/controllers/port/actuator.go b/internal/controllers/port/actuator.go index 13136eba..1289c28a 100644 --- a/internal/controllers/port/actuator.go +++ b/internal/controllers/port/actuator.go @@ -23,6 +23,7 @@ import ( "iter" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -199,8 +200,8 @@ func (portHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { } func (portHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, createResourceActuator, error) { - waitEvents, actuator, err := newCreateActuator(ctx, orcObject, controller) - return waitEvents, actuator, err + actuator, progressStatus, err := newCreateActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } func (portHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, deleteResourceActuator, error) { @@ -225,14 +226,20 @@ func newActuator(ctx context.Context, orcObject *orcv1alpha1.Port, controller ge }, nil } -func newCreateActuator(ctx context.Context, orcObject *orcv1alpha1.Port, controller generic.ResourceController) ([]generic.ProgressStatus, *portCreateActuator, error) { +func newCreateActuator(ctx context.Context, orcObject *orcv1alpha1.Port, controller generic.ResourceController) (*portCreateActuator, []generic.ProgressStatus, error) { + // Ensure credential secrets exist and have our finalizer + _, progressStatus, err := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if len(progressStatus) > 0 || err != nil { + return nil, progressStatus, err + } + orcNetwork, progressStatus, err := networkDependency.GetDependency( ctx, controller.GetK8sClient(), orcObject, func(dep *orcv1alpha1.Network) bool { return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil }, ) if len(progressStatus) != 0 || err != nil { - return progressStatus, nil, err + return nil, progressStatus, err } networkID := *orcNetwork.Status.ID @@ -241,9 +248,9 @@ func newCreateActuator(ctx context.Context, orcObject *orcv1alpha1.Port, control return nil, nil, err } - return nil, &portCreateActuator{ + return &portCreateActuator{ portActuator: portActuator, k8sClient: controller.GetK8sClient(), networkID: networkID, - }, nil + }, nil, nil } diff --git a/internal/controllers/port/controller.go b/internal/controllers/port/controller.go index cb100543..da851632 100644 --- a/internal/controllers/port/controller.go +++ b/internal/controllers/port/controller.go @@ -30,6 +30,7 @@ import ( ctrlexport "github.com/k-orc/openstack-resource-controller/internal/controllers/export" "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/scope" + "github.com/k-orc/openstack-resource-controller/internal/util/credentials" "github.com/k-orc/openstack-resource-controller/internal/util/dependency" ) @@ -39,15 +40,12 @@ import ( const controllerName = "port" var ( - finalizer = generic.GetFinalizerName(controllerName) - fieldOwner = generic.GetSSAFieldOwner(controllerName) - networkDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.PortList, *orcv1alpha1.Network]( "spec.resource.networkRef", func(port *orcv1alpha1.Port) []string { return []string{string(port.Spec.NetworkRef)} }, - finalizer, fieldOwner, + finalizer, externalObjectFieldOwner, ) subnetDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.PortList, *orcv1alpha1.Subnet]( @@ -59,7 +57,7 @@ var ( } return subnets }, - finalizer, fieldOwner, + finalizer, externalObjectFieldOwner, ) securityGroupDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.PortList, *orcv1alpha1.SecurityGroup]( @@ -71,7 +69,7 @@ var ( } return securityGroups }, - finalizer, fieldOwner, + finalizer, externalObjectFieldOwner, ) ) @@ -92,14 +90,6 @@ func (c portReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr log := mgr.GetLogger().WithValues("controller", controllerName) k8sClient := mgr.GetClient() - if err := errors.Join( - networkDependency.AddToManager(ctx, mgr), - subnetDependency.AddToManager(ctx, mgr), - securityGroupDependency.AddToManager(ctx, mgr), - ); err != nil { - return err - } - networkWatchEventHandler, err := networkDependency.WatchEventHandler(log, k8sClient) if err != nil { return err @@ -115,8 +105,8 @@ func (c portReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr return err } - reconciler := generic.NewController(controllerName, k8sClient, c.scopeFactory, portHelperFactory{}, portStatusWriter{}) - return ctrl.NewControllerManagedBy(mgr). + builder := ctrl.NewControllerManagedBy(mgr). + WithOptions(options). For(&orcv1alpha1.Port{}). Watches(&orcv1alpha1.Network{}, networkWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Network{})), @@ -126,7 +116,18 @@ func (c portReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr ). Watches(&orcv1alpha1.SecurityGroup{}, securityGroupWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.SecurityGroup{})), - ). - WithOptions(options). - Complete(&reconciler) + ) + + if err := errors.Join( + networkDependency.AddToManager(ctx, mgr), + subnetDependency.AddToManager(ctx, mgr), + securityGroupDependency.AddToManager(ctx, mgr), + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, k8sClient, builder, credentialsDependency), + ); err != nil { + return err + } + + reconciler := generic.NewController(controllerName, k8sClient, c.scopeFactory, portHelperFactory{}, portStatusWriter{}) + return builder.Complete(&reconciler) } diff --git a/internal/controllers/port/zz_generated.adapter.go b/internal/controllers/port/zz_generated.adapter.go index 0797761c..276e3869 100644 --- a/internal/controllers/port/zz_generated.adapter.go +++ b/internal/controllers/port/zz_generated.adapter.go @@ -24,9 +24,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.Port - resourceSpecT = orcv1alpha1.PortResourceSpec - filterT = orcv1alpha1.PortFilter + orcObjectT = orcv1alpha1.Port + orcObjectListT = orcv1alpha1.PortList + resourceSpecT = orcv1alpha1.PortResourceSpec + filterT = orcv1alpha1.PortFilter ) // Derived types diff --git a/internal/controllers/port/zz_generated.controller.go b/internal/controllers/port/zz_generated.controller.go new file mode 100644 index 00000000..723e39ab --- /dev/null +++ b/internal/controllers/port/zz_generated.controller.go @@ -0,0 +1,45 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package port + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/internal/controllers/router/actuator.go b/internal/controllers/router/actuator.go index df8eab81..87f85c6e 100644 --- a/internal/controllers/router/actuator.go +++ b/internal/controllers/router/actuator.go @@ -21,6 +21,7 @@ import ( "iter" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -153,40 +154,46 @@ func (routerHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { } func (routerHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, createResourceActuator, error) { - actuator, err := newCreateActuator(ctx, orcObject, controller) - return nil, actuator, err + actuator, progressStatus, err := newCreateActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } func (routerHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, deleteResourceActuator, error) { - actuator, err := newActuator(ctx, orcObject, controller) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } -func newActuator(ctx context.Context, orcObject *orcv1alpha1.Router, controller generic.ResourceController) (routerActuator, error) { +func newActuator(ctx context.Context, orcObject *orcv1alpha1.Router, controller generic.ResourceController) (routerActuator, []generic.ProgressStatus, error) { log := ctrl.LoggerFrom(ctx) + // Ensure credential secrets exist and have our finalizer + _, progressStatus, err := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if len(progressStatus) > 0 || err != nil { + return routerActuator{}, progressStatus, err + } + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) if err != nil { - return routerActuator{}, err + return routerActuator{}, nil, err } osClient, err := clientScope.NewNetworkClient() if err != nil { - return routerActuator{}, err + return routerActuator{}, nil, err } return routerActuator{ osClient: osClient, - }, nil + }, nil, nil } -func newCreateActuator(ctx context.Context, orcObject *orcv1alpha1.Router, controller generic.ResourceController) (routerCreateActuator, error) { - routerActuator, err := newActuator(ctx, orcObject, controller) - if err != nil { - return routerCreateActuator{}, err +func newCreateActuator(ctx context.Context, orcObject *orcv1alpha1.Router, controller generic.ResourceController) (routerCreateActuator, []generic.ProgressStatus, error) { + routerActuator, progressStatus, err := newActuator(ctx, orcObject, controller) + if len(progressStatus) > 0 || err != nil { + return routerCreateActuator{}, progressStatus, err } return routerCreateActuator{ routerActuator: routerActuator, k8sClient: controller.GetK8sClient(), - }, nil + }, nil, nil } diff --git a/internal/controllers/router/controller.go b/internal/controllers/router/controller.go index 2c94c98e..8c08b3a8 100644 --- a/internal/controllers/router/controller.go +++ b/internal/controllers/router/controller.go @@ -30,6 +30,7 @@ import ( ctrlexport "github.com/k-orc/openstack-resource-controller/internal/controllers/export" "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/scope" + "github.com/k-orc/openstack-resource-controller/internal/util/credentials" "github.com/k-orc/openstack-resource-controller/internal/util/dependency" ) @@ -51,9 +52,6 @@ func (routerReconcilerConstructor) GetName() string { const controllerName = "router" var ( - finalizer = generic.GetFinalizerName(controllerName) - fieldOwner = generic.GetSSAFieldOwner(controllerName) - // Router depends on its external gateways, which are Networks externalGWDep = dependency.NewDeletionGuardDependency[*orcv1alpha1.RouterList, *orcv1alpha1.Network]( "spec.resource.externalGateways[].networkRef", @@ -69,7 +67,7 @@ var ( } return networks }, - finalizer, fieldOwner, + finalizer, externalObjectFieldOwner, ) ) @@ -78,23 +76,26 @@ func (c routerReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c log := mgr.GetLogger().WithValues("controller", controllerName) k8sClient := mgr.GetClient() - if err := errors.Join( - externalGWDep.AddToManager(ctx, mgr), - ); err != nil { - return err - } - externalGWHandler, err := externalGWDep.WatchEventHandler(log, k8sClient) if err != nil { return err } - reconciler := generic.NewController(controllerName, k8sClient, c.scopeFactory, routerHelperFactory{}, routerStatusWriter{}) - return ctrl.NewControllerManagedBy(mgr). + builder := ctrl.NewControllerManagedBy(mgr). + WithOptions(options). For(&orcv1alpha1.Router{}). Watches(&orcv1alpha1.Network{}, externalGWHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Network{})), - ). - WithOptions(options). - Complete(&reconciler) + ) + + if err := errors.Join( + externalGWDep.AddToManager(ctx, mgr), + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, k8sClient, builder, credentialsDependency), + ); err != nil { + return err + } + + reconciler := generic.NewController(controllerName, k8sClient, c.scopeFactory, routerHelperFactory{}, routerStatusWriter{}) + return builder.Complete(&reconciler) } diff --git a/internal/controllers/router/zz_generated.adapter.go b/internal/controllers/router/zz_generated.adapter.go index c3bd4599..46a42f77 100644 --- a/internal/controllers/router/zz_generated.adapter.go +++ b/internal/controllers/router/zz_generated.adapter.go @@ -24,9 +24,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.Router - resourceSpecT = orcv1alpha1.RouterResourceSpec - filterT = orcv1alpha1.RouterFilter + orcObjectT = orcv1alpha1.Router + orcObjectListT = orcv1alpha1.RouterList + resourceSpecT = orcv1alpha1.RouterResourceSpec + filterT = orcv1alpha1.RouterFilter ) // Derived types diff --git a/internal/controllers/router/zz_generated.controller.go b/internal/controllers/router/zz_generated.controller.go new file mode 100644 index 00000000..2628b85a --- /dev/null +++ b/internal/controllers/router/zz_generated.controller.go @@ -0,0 +1,45 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package router + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/internal/controllers/securitygroup/actuator.go b/internal/controllers/securitygroup/actuator.go index 98a5bad9..28def2d1 100644 --- a/internal/controllers/securitygroup/actuator.go +++ b/internal/controllers/securitygroup/actuator.go @@ -30,6 +30,7 @@ import ( osclients "github.com/k-orc/openstack-resource-controller/internal/osclients" orcerrors "github.com/k-orc/openstack-resource-controller/internal/util/errors" "github.com/k-orc/openstack-resource-controller/internal/util/neutrontags" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" "k8s.io/utils/set" ctrl "sigs.k8s.io/controller-runtime" @@ -247,28 +248,34 @@ func (securityGroupHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI } func (securityGroupHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, createResourceActuator, error) { - actuator, err := newActuator(ctx, orcObject, controller) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } func (securityGroupHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, deleteResourceActuator, error) { - actuator, err := newActuator(ctx, orcObject, controller) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, orcObject, controller) + return progressStatus, actuator, err } -func newActuator(ctx context.Context, orcObject *orcv1alpha1.SecurityGroup, controller generic.ResourceController) (securityGroupActuator, error) { +func newActuator(ctx context.Context, orcObject *orcv1alpha1.SecurityGroup, controller generic.ResourceController) (securityGroupActuator, []generic.ProgressStatus, error) { log := ctrl.LoggerFrom(ctx) + // Ensure credential secrets exist and have our finalizer + _, progressStatus, err := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if len(progressStatus) > 0 || err != nil { + return securityGroupActuator{}, progressStatus, err + } + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) if err != nil { - return securityGroupActuator{}, err + return securityGroupActuator{}, nil, err } osClient, err := clientScope.NewNetworkClient() if err != nil { - return securityGroupActuator{}, err + return securityGroupActuator{}, nil, err } return securityGroupActuator{ osClient: osClient, - }, nil + }, nil, nil } diff --git a/internal/controllers/securitygroup/controller.go b/internal/controllers/securitygroup/controller.go index e3924428..80bdbac1 100644 --- a/internal/controllers/securitygroup/controller.go +++ b/internal/controllers/securitygroup/controller.go @@ -18,6 +18,7 @@ package securitygroup import ( "context" + "errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -27,8 +28,11 @@ import ( ctrlexport "github.com/k-orc/openstack-resource-controller/internal/controllers/export" "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/scope" + "github.com/k-orc/openstack-resource-controller/internal/util/credentials" ) +const controllerName = "securitygroup" + // +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=securitygroups,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=securitygroups/status,verbs=get;update;patch @@ -43,16 +47,25 @@ func New(scopeFactory scope.Factory) ctrlexport.Controller { } func (securitygroupReconcilerConstructor) GetName() string { - return "securitygroup" + return controllerName } // SetupWithManager sets up the controller with the Manager. -func (c securitygroupReconcilerConstructor) SetupWithManager(_ context.Context, mgr ctrl.Manager, options controller.Options) error { - reconciler := generic.NewController(c.GetName(), mgr.GetClient(), c.scopeFactory, securityGroupHelperFactory{}, securityGroupStatusWriter{}) +func (c securitygroupReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + log := ctrl.LoggerFrom(ctx) - return ctrl.NewControllerManagedBy(mgr). - For(&orcv1alpha1.SecurityGroup{}). + builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). - Complete(&reconciler) + For(&orcv1alpha1.SecurityGroup{}) + + if err := errors.Join( + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), + ); err != nil { + return err + } + + reconciler := generic.NewController(controllerName, mgr.GetClient(), c.scopeFactory, securityGroupHelperFactory{}, securityGroupStatusWriter{}) + return builder.Complete(&reconciler) } diff --git a/internal/controllers/securitygroup/zz_generated.adapter.go b/internal/controllers/securitygroup/zz_generated.adapter.go index f9e976e4..2041af20 100644 --- a/internal/controllers/securitygroup/zz_generated.adapter.go +++ b/internal/controllers/securitygroup/zz_generated.adapter.go @@ -24,9 +24,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.SecurityGroup - resourceSpecT = orcv1alpha1.SecurityGroupResourceSpec - filterT = orcv1alpha1.SecurityGroupFilter + orcObjectT = orcv1alpha1.SecurityGroup + orcObjectListT = orcv1alpha1.SecurityGroupList + resourceSpecT = orcv1alpha1.SecurityGroupResourceSpec + filterT = orcv1alpha1.SecurityGroupFilter ) // Derived types diff --git a/internal/controllers/securitygroup/zz_generated.controller.go b/internal/controllers/securitygroup/zz_generated.controller.go new file mode 100644 index 00000000..1586a714 --- /dev/null +++ b/internal/controllers/securitygroup/zz_generated.controller.go @@ -0,0 +1,45 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package securitygroup + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/internal/controllers/server/actuator.go b/internal/controllers/server/actuator.go index 7f61a1ca..735ba50b 100644 --- a/internal/controllers/server/actuator.go +++ b/internal/controllers/server/actuator.go @@ -240,32 +240,39 @@ func (serverHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { } func (serverHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, createResourceActuator, error) { - actuator, err := newActuator(ctx, controller, orcObject) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, controller, orcObject) + return progressStatus, actuator, err } func (serverHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, deleteResourceActuator, error) { - actuator, err := newActuator(ctx, controller, orcObject) - return nil, actuator, err + actuator, progressStatus, err := newActuator(ctx, controller, orcObject) + return progressStatus, actuator, err } -func newActuator(ctx context.Context, controller generic.ResourceController, orcObject *orcv1alpha1.Server) (serverActuator, error) { +func newActuator(ctx context.Context, controller generic.ResourceController, orcObject *orcv1alpha1.Server) (serverActuator, []generic.ProgressStatus, error) { if orcObject == nil { - return serverActuator{}, fmt.Errorf("orcObject may not be nil") + return serverActuator{}, nil, fmt.Errorf("orcObject may not be nil") } log := ctrl.LoggerFrom(ctx) + + // Ensure credential secrets exist and have our finalizer + _, progressStatus, err := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if len(progressStatus) > 0 || err != nil { + return serverActuator{}, progressStatus, err + } + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) if err != nil { - return serverActuator{}, err + return serverActuator{}, nil, err } osClient, err := clientScope.NewComputeClient() if err != nil { - return serverActuator{}, err + return serverActuator{}, nil, err } return serverActuator{ osClient: osClient, k8sClient: controller.GetK8sClient(), - }, nil + }, nil, nil } diff --git a/internal/controllers/server/controller.go b/internal/controllers/server/controller.go index e1e5a058..aac93560 100644 --- a/internal/controllers/server/controller.go +++ b/internal/controllers/server/controller.go @@ -29,6 +29,7 @@ import ( ctrlexport "github.com/k-orc/openstack-resource-controller/internal/controllers/export" "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/scope" + "github.com/k-orc/openstack-resource-controller/internal/util/credentials" "github.com/k-orc/openstack-resource-controller/internal/util/dependency" "github.com/k-orc/openstack-resource-controller/pkg/predicates" ) @@ -51,9 +52,6 @@ func (serverReconcilerConstructor) GetName() string { const controllerName = "server" var ( - finalizer = generic.GetFinalizerName(controllerName) - fieldOwner = generic.GetSSAFieldOwner(controllerName) - // No deletion guard for flavor, because flavors can be safely deleted while // referenced by a server flavorDependency = dependency.NewDependency[*orcv1alpha1.ServerList, *orcv1alpha1.Flavor]( @@ -81,7 +79,7 @@ var ( return []string{string(resource.ImageRef)} }, - finalizer, fieldOwner, + finalizer, externalObjectFieldOwner, ) portDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.ServerList, *orcv1alpha1.Port]( @@ -101,7 +99,7 @@ var ( } return refs }, - finalizer, fieldOwner, + finalizer, externalObjectFieldOwner, ) // We don't need a deletion guard on the user-data secret because it's only @@ -124,15 +122,6 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c log := mgr.GetLogger().WithValues("controller", controllerName) k8sClient := mgr.GetClient() - if err := errors.Join( - flavorDependency.AddToManager(ctx, mgr), - imageDependency.AddToManager(ctx, mgr), - portDependency.AddToManager(ctx, mgr), - userDataDependency.AddToManager(ctx, mgr), - ); err != nil { - return err - } - flavorWatchEventHandler, err := flavorDependency.WatchEventHandler(log, k8sClient) if err != nil { return err @@ -150,10 +139,9 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c return err } - reconciler := generic.NewController(controllerName, k8sClient, c.scopeFactory, serverHelperFactory{}, serverStatusWriter{}) - return ctrl.NewControllerManagedBy(mgr). - For(&orcv1alpha1.Server{}). + builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). + For(&orcv1alpha1.Server{}). Watches(&orcv1alpha1.Flavor{}, flavorWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Flavor{})), ). @@ -170,6 +158,19 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c // // These will require separate solutions. For the latter we should // probably use a MetadataOnly watch only secrets. - Watches(&corev1.Secret{}, userDataWatchEventHandler). - Complete(&reconciler) + Watches(&corev1.Secret{}, userDataWatchEventHandler) + + if err := errors.Join( + flavorDependency.AddToManager(ctx, mgr), + imageDependency.AddToManager(ctx, mgr), + portDependency.AddToManager(ctx, mgr), + userDataDependency.AddToManager(ctx, mgr), + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, k8sClient, builder, credentialsDependency), + ); err != nil { + return err + } + + reconciler := generic.NewController(controllerName, k8sClient, c.scopeFactory, serverHelperFactory{}, serverStatusWriter{}) + return builder.Complete(&reconciler) } diff --git a/internal/controllers/server/zz_generated.adapter.go b/internal/controllers/server/zz_generated.adapter.go index 435d11dc..c26f254e 100644 --- a/internal/controllers/server/zz_generated.adapter.go +++ b/internal/controllers/server/zz_generated.adapter.go @@ -24,9 +24,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.Server - resourceSpecT = orcv1alpha1.ServerResourceSpec - filterT = orcv1alpha1.ServerFilter + orcObjectT = orcv1alpha1.Server + orcObjectListT = orcv1alpha1.ServerList + resourceSpecT = orcv1alpha1.ServerResourceSpec + filterT = orcv1alpha1.ServerFilter ) // Derived types diff --git a/internal/controllers/server/zz_generated.controller.go b/internal/controllers/server/zz_generated.controller.go new file mode 100644 index 00000000..a0a32481 --- /dev/null +++ b/internal/controllers/server/zz_generated.controller.go @@ -0,0 +1,45 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/internal/controllers/subnet/actuator.go b/internal/controllers/subnet/actuator.go index 70879229..e3e230d7 100644 --- a/internal/controllers/subnet/actuator.go +++ b/internal/controllers/subnet/actuator.go @@ -23,6 +23,7 @@ import ( "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets" + 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/types" @@ -314,9 +315,9 @@ func (subnetHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcO return progressStatus, nil, err } - actuator, err := newActuator(ctx, controller, orcObject) - if err != nil { - return nil, nil, err + actuator, progressStatus, err := newActuator(ctx, controller, orcObject) + if len(progressStatus) > 0 || err != nil { + return progressStatus, nil, err } return nil, subnetCreateActuator{ subnetActuator: actuator, @@ -325,32 +326,38 @@ func (subnetHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcO } func (subnetHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) ([]generic.ProgressStatus, deleteResourceActuator, error) { - actuator, err := newActuator(ctx, controller, orcObject) - if err != nil { - return nil, nil, err + actuator, progressStatus, err := newActuator(ctx, controller, orcObject) + if len(progressStatus) > 0 || err != nil { + return progressStatus, nil, err } return nil, subnetDeleteActuator{ subnetActuator: actuator, }, nil } -func newActuator(ctx context.Context, controller generic.ResourceController, orcObject *orcv1alpha1.Subnet) (subnetActuator, error) { +func newActuator(ctx context.Context, controller generic.ResourceController, orcObject *orcv1alpha1.Subnet) (subnetActuator, []generic.ProgressStatus, error) { if orcObject == nil { - return subnetActuator{}, fmt.Errorf("orcObject may not be nil") + return subnetActuator{}, nil, fmt.Errorf("orcObject may not be nil") + } + + // Ensure credential secrets exist and have our finalizer + _, progressStatus, err := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if len(progressStatus) > 0 || err != nil { + return subnetActuator{}, progressStatus, err } log := ctrl.LoggerFrom(ctx) clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) if err != nil { - return subnetActuator{}, err + return subnetActuator{}, nil, err } osClient, err := clientScope.NewNetworkClient() if err != nil { - return subnetActuator{}, err + return subnetActuator{}, nil, err } return subnetActuator{ osClient: osClient, k8sClient: controller.GetK8sClient(), - }, nil + }, nil, nil } diff --git a/internal/controllers/subnet/controller.go b/internal/controllers/subnet/controller.go index 0be6bfd2..80b39a5a 100644 --- a/internal/controllers/subnet/controller.go +++ b/internal/controllers/subnet/controller.go @@ -30,12 +30,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" - "github.com/k-orc/openstack-resource-controller/pkg/predicates" - ctrlexport "github.com/k-orc/openstack-resource-controller/internal/controllers/export" "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" "github.com/k-orc/openstack-resource-controller/internal/scope" + "github.com/k-orc/openstack-resource-controller/internal/util/credentials" "github.com/k-orc/openstack-resource-controller/internal/util/dependency" + "github.com/k-orc/openstack-resource-controller/pkg/predicates" ) type subnetReconcilerConstructor struct { @@ -53,15 +53,12 @@ func (subnetReconcilerConstructor) GetName() string { const controllerName = "subnet" var ( - finalizer = generic.GetFinalizerName(controllerName) - fieldOwner = generic.GetSSAFieldOwner(controllerName) - networkDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.SubnetList, *orcv1alpha1.Network]( "spec.resource.networkRef", func(subnet *orcv1alpha1.Subnet) []string { return []string{string(subnet.Spec.NetworkRef)} }, - finalizer, fieldOwner, + finalizer, externalObjectFieldOwner, ) ) @@ -71,19 +68,13 @@ func (c subnetReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c log := mgr.GetLogger().WithValues("controller", controllerName) k8sClient := mgr.GetClient() - if err := errors.Join( - networkDependency.AddToManager(ctx, mgr), - ); err != nil { - return err - } - networkWatchEventHandler, err := networkDependency.WatchEventHandler(log, k8sClient) if err != nil { return err } - reconciler := generic.NewController(controllerName, k8sClient, c.scopeFactory, subnetHelperFactory{}, subnetStatusWriter{}) - return ctrl.NewControllerManagedBy(mgr). + builder := ctrl.NewControllerManagedBy(mgr). + WithOptions(options). For(&orcv1alpha1.Subnet{}). Watches(&orcv1alpha1.Network{}, networkWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Network{})), @@ -105,7 +96,16 @@ func (c subnetReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c } }), builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.RouterInterface{})), - ). - WithOptions(options). - Complete(&reconciler) + ) + + if err := errors.Join( + networkDependency.AddToManager(ctx, mgr), + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, k8sClient, builder, credentialsDependency), + ); err != nil { + return err + } + + reconciler := generic.NewController(controllerName, k8sClient, c.scopeFactory, subnetHelperFactory{}, subnetStatusWriter{}) + return builder.Complete(&reconciler) } diff --git a/internal/controllers/subnet/zz_generated.adapter.go b/internal/controllers/subnet/zz_generated.adapter.go index baddd973..6614774c 100644 --- a/internal/controllers/subnet/zz_generated.adapter.go +++ b/internal/controllers/subnet/zz_generated.adapter.go @@ -24,9 +24,10 @@ import ( // Fundamental types type ( - orcObjectT = orcv1alpha1.Subnet - resourceSpecT = orcv1alpha1.SubnetResourceSpec - filterT = orcv1alpha1.SubnetFilter + orcObjectT = orcv1alpha1.Subnet + orcObjectListT = orcv1alpha1.SubnetList + resourceSpecT = orcv1alpha1.SubnetResourceSpec + filterT = orcv1alpha1.SubnetFilter ) // Derived types diff --git a/internal/controllers/subnet/zz_generated.controller.go b/internal/controllers/subnet/zz_generated.controller.go new file mode 100644 index 00000000..19432700 --- /dev/null +++ b/internal/controllers/subnet/zz_generated.controller.go @@ -0,0 +1,45 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright 2024 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package subnet + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +var ( + // NOTE: controllerName must be defined in any controller using this template + + // finalizer is the string this controller adds to an object's Finalizers + finalizer = generic.GetFinalizerName(controllerName) + + // externalObjectFieldOwner is the field owner we use when using + // server-side-apply on objects we don't control + externalObjectFieldOwner = generic.GetSSAFieldOwner(controllerName) + + credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret]( + "spec.cloudCredentialsRef.secretName", + func(obj orcObjectPT) []string { + return []string{obj.Spec.CloudCredentialsRef.SecretName} + }, + finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("credentials"), + ) +) \ No newline at end of file diff --git a/internal/scope/provider.go b/internal/scope/provider.go index 5030195e..7620a18e 100644 --- a/internal/scope/provider.go +++ b/internal/scope/provider.go @@ -239,8 +239,6 @@ func (g gophercloudLogger) Printf(format string, args ...interface{}) { g.logger.Info(fmt.Sprintf(format, args...)) } -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch - // getCloudFromSecret extract a Cloud from the given namespace:secretName. func getCloudFromSecret(ctx context.Context, ctrlClient client.Client, secretNamespace string, secretName string, cloudName string) (clientconfig.Cloud, []byte, error) { emptyCloud := clientconfig.Cloud{} diff --git a/internal/util/credentials/dependency.go b/internal/util/credentials/dependency.go new file mode 100644 index 00000000..c16a7df6 --- /dev/null +++ b/internal/util/credentials/dependency.go @@ -0,0 +1,74 @@ +/* +Copyright 2025 The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package credentials + +import ( + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/k-orc/openstack-resource-controller/internal/util/dependency" +) + +/* + NOTE: These are cluster-wide permissions on secrets, which is not ideal. + + On the update privilege: we only need this for adding finalizers. Although + the OwnerReferencesPermissionEnforcement + (https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement) + may make it look like there is, there is no real finalizers subresource, so + we can't confine this to the ability to write the finalizer. + + I (mdbooth) suspect that the future may bring new capabilities, either in + kube or its supporting ecosystem, which may allow us to reduce these + privileges in the future. We should periodically take some time to find out + if that has happened yet. +*/ + +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=update;patch + +func AddCredentialsWatch[ + objectTP dependency.ObjectType[objectT], + objectListTP dependency.ObjectListType[objectListT, objectT], + depTP dependency.DependencyType[depT], + + objectT any, objectListT any, depT any, +]( + log logr.Logger, + k8sClient client.Client, + b *builder.Builder, + credentialsDep dependency.DeletionGuardDependency[objectTP, objectListTP, depTP, objectT, objectListT, depT], +) error { + credentialsWatchEventHandler, err := credentialsDep.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } + + b.Watches(&corev1.Secret{}, credentialsWatchEventHandler, + // Only trigger a reconcile when the credentials are created. We + // don't need to reconcile for updates. + builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(_ event.TypedCreateEvent[client.Object]) bool { + return true + }, + })) + + return nil +} diff --git a/internal/util/dependency/deletion_guard.go b/internal/util/dependency/deletion_guard.go index 271e7c6b..592e3ce3 100644 --- a/internal/util/dependency/deletion_guard.go +++ b/internal/util/dependency/deletion_guard.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -42,11 +43,28 @@ import ( // deleted if it is still in use by any Subnet. It is added by the Subnet // controller, but it is a separate controller which reconciles Network objects. -func addDeletionGuard[objTP objectType[objT], objT any, depTP objectType[depT], depT any]( +func addDeletionGuard[objTP ObjectType[objT], objT any, depTP ObjectType[depT], depT any]( mgr ctrl.Manager, finalizer string, fieldOwner client.FieldOwner, getDepRefsFromObject func(client.Object) []string, getObjectsFromDep func(context.Context, client.Client, depTP) ([]objT, error), + overrideDependencyName *string, ) error { + var depSpecimen depTP = new(depT) + var objSpecimen objTP = new(objT) + + scheme := mgr.GetScheme() + depKind, err := getObjectKind(depSpecimen, scheme) + if err != nil { + return err + } + objKind, err := getObjectKind(objSpecimen, scheme) + if err != nil { + return err + } + + dependencyName := ptr.Deref(overrideDependencyName, strings.ToLower(depKind)) + controllerName := dependencyName + "_deletion_guard_for_" + strings.ToLower(objKind) + // deletionGuard reconciles the dependency object // If the dependency is marked deleted, we remove the finalizer only when there are no objects referencing it deletionGuard := reconcile.Func(func(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { @@ -78,7 +96,6 @@ func addDeletionGuard[objTP objectType[objT], objT any, depTP objectType[depT], return reconcile.Result{}, nil } - depKind := dep.GetObjectKind().GroupVersionKind().Kind depUID := dep.GetUID() depOwns := func(obj objTP) bool { owners := obj.GetOwnerReferences() @@ -107,21 +124,6 @@ func addDeletionGuard[objTP objectType[objT], objT any, depTP objectType[depT], return ctrl.Result{}, k8sClient.Patch(ctx, dep, patch, client.ForceOwnership, fieldOwner) }) - var depSpecimen depTP = new(depT) - var objSpecimen objTP = new(objT) - - scheme := mgr.GetScheme() - depKind, err := getObjectKind(depSpecimen, scheme) - if err != nil { - return err - } - objKind, err := getObjectKind(objSpecimen, scheme) - if err != nil { - return err - } - - controllerName := strings.ToLower(depKind) + "_deletion_guard_for_" + strings.ToLower(objKind) - // Register deletionGuard with the manager as a reconciler of the // dependency. We also watch for referring objects, but we're only // interested in deletion events. We need to ensure that if the depdency diff --git a/internal/util/dependency/dependency.go b/internal/util/dependency/dependency.go index 4edb390b..ef85f5a0 100644 --- a/internal/util/dependency/dependency.go +++ b/internal/util/dependency/dependency.go @@ -57,10 +57,10 @@ import ( // - indexName: "spec.resource.addresses[].subnetRef" - a symbolic path to the subnet reference in a Port // - getDependencyRefs: func(object *Port) []string{ ... returns a slice containing all subnetRefs in this Port's addresses ... } func NewDependency[ - objectListTP objectListType[objectListT, objectT], - depTP dependencyType[depT], + objectListTP ObjectListType[objectListT, objectT], + depTP DependencyType[depT], - objectTP objectType[objectT], + objectTP ObjectType[objectT], objectT any, objectListT any, depT any, ](indexName string, getDependencyRefs func(objectTP) []string) Dependency[objectTP, objectListTP, depTP, objectT, objectListT, depT] { return Dependency[objectTP, objectListTP, depTP, objectT, objectListT, depT]{ @@ -69,29 +69,47 @@ func NewDependency[ } } +type deletionGuardConfig struct { + overrideDependencyName *string +} + +type deletionGuardOpt = func(*deletionGuardConfig) + +func OverrideDependencyName(name string) deletionGuardOpt { + return func(opts *deletionGuardConfig) { + opts.overrideDependencyName = &name + } +} + // NewDeletionGuardDependency returns a Dependency which can additionally create a deletion guard for the dependency. See NewDependency for a discussion of the base functionality. // // In addition to the arguments required by NewDependency, NewDeletionGuardDependency requires: // - finalizer: the string to add to Finalizers in objects that we depend on // - fieldOwner: a client.FieldOwner identifying this controller when adding a finalizer to objects we depend on func NewDeletionGuardDependency[ - objectListTP objectListType[objectListT, objectT], - depTP dependencyType[depT], + objectListTP ObjectListType[objectListT, objectT], + depTP DependencyType[depT], - objectTP objectType[objectT], + objectTP ObjectType[objectT], objectT any, objectListT any, depT any, -](indexName string, getDependencyRefs func(objectTP) []string, finalizer string, fieldOwner client.FieldOwner) DeletionGuardDependency[objectTP, objectListTP, depTP, objectT, objectListT, depT] { +](indexName string, getDependencyRefs func(objectTP) []string, finalizer string, fieldOwner client.FieldOwner, opts ...deletionGuardOpt) DeletionGuardDependency[objectTP, objectListTP, depTP, objectT, objectListT, depT] { + config := deletionGuardConfig{} + for _, opt := range opts { + opt(&config) + } + return DeletionGuardDependency[objectTP, objectListTP, depTP, objectT, objectListT, depT]{ - Dependency: NewDependency[objectListTP, depTP](indexName, getDependencyRefs), - finalizer: finalizer, - fieldOwner: fieldOwner, + Dependency: NewDependency[objectListTP, depTP](indexName, getDependencyRefs), + finalizer: finalizer, + fieldOwner: fieldOwner, + overrideDependencyName: config.overrideDependencyName, } } type Dependency[ - objectTP objectType[objectT], - objectListTP objectListType[objectListT, objectT], - depTP dependencyType[depT], + objectTP ObjectType[objectT], + objectListTP ObjectListType[objectListT, objectT], + depTP DependencyType[depT], objectT any, objectListT any, depT any, ] struct { @@ -100,31 +118,32 @@ type Dependency[ } type DeletionGuardDependency[ - objectTP objectType[objectT], - objectListTP objectListType[objectListT, objectT], - depTP dependencyType[depT], + objectTP ObjectType[objectT], + objectListTP ObjectListType[objectListT, objectT], + depTP DependencyType[depT], objectT any, objectListT any, depT any, ] struct { Dependency[objectTP, objectListTP, depTP, objectT, objectListT, depT] - finalizer string - fieldOwner client.FieldOwner + finalizer string + fieldOwner client.FieldOwner + overrideDependencyName *string } -type objectType[objectT any] interface { +type ObjectType[objectT any] interface { *objectT client.Object } -type objectListType[objectListT any, objectT any] interface { +type ObjectListType[objectListT any, objectT any] interface { client.ObjectList *objectListT GetItems() []objectT } -type dependencyType[depT any] interface { +type DependencyType[depT any] interface { *depT client.Object } @@ -200,7 +219,7 @@ func (d *DeletionGuardDependency[objectTP, _, _, _, _, _]) addDeletionGuard(mgr return d.getDependencyRefs(obj) } - return addDeletionGuard[objectTP](mgr, d.finalizer, d.fieldOwner, getDependencyRefsForClientObject, d.GetObjectsForDependency) + return addDeletionGuard[objectTP](mgr, d.finalizer, d.fieldOwner, getDependencyRefsForClientObject, d.GetObjectsForDependency, d.overrideDependencyName) } // GetDependencies returns the dependencies of the given object, ensuring that all returned dependencies have the required finalizer. It returns: