From 5f895e430b7a271c8455d5e838d6e5eb33fba4ea Mon Sep 17 00:00:00 2001 From: Nico Schieder Date: Wed, 30 Apr 2025 16:45:36 +0200 Subject: [PATCH 01/10] Implement Boxcutter applier, revision controller --- api/v1/clusterextensionrevision_types.go | 122 +++++ api/v1/zz_generated.deepcopy.go | 161 +++++++ cmd/operator-controller/main.go | 44 +- go.mod | 7 +- go.sum | 10 +- .../operator-controller/applier/boxcutter.go | 225 ++++++++++ .../clusterextensionrevision_controller.go | 420 ++++++++++++++++++ 7 files changed, 981 insertions(+), 8 deletions(-) create mode 100644 api/v1/clusterextensionrevision_types.go create mode 100644 internal/operator-controller/applier/boxcutter.go create mode 100644 internal/operator-controller/controllers/clusterextensionrevision_controller.go diff --git a/api/v1/clusterextensionrevision_types.go b/api/v1/clusterextensionrevision_types.go new file mode 100644 index 000000000..1322f9874 --- /dev/null +++ b/api/v1/clusterextensionrevision_types.go @@ -0,0 +1,122 @@ +/* +Copyright 2024. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" +) + +const ClusterExtensionRevisionKind = "ClusterExtensionRevision" + +// ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision. +type ClusterExtensionRevisionSpec struct { + // Specifies the lifecycle state of the ClusterExtensionRevision. + // +kubebuilder:default="Active" + // +kubebuilder:validation:Enum=Active;Paused;Archived + // +kubebuilder:validation:XValidation:rule="oldSelf == "Active" || oldSelf == "Paused" || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive" + LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" + Revision int64 `json:"revision"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="phases is immutable" + Phases []ClusterExtensionRevisionPhase `json:"phases"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable" + Previous []ClusterExtensionRevisionPrevious `json:"previous"` +} + +// ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision. +type ClusterExtensionRevisionLifecycleState string + +const ( + // ClusterExtensionRevisionLifecycleStateActive / "Active" is the default lifecycle state. + ClusterExtensionRevisionLifecycleStateActive ClusterExtensionRevisionLifecycleState = "Active" + // ClusterExtensionRevisionLifecycleStatePaused / "Paused" disables reconciliation of the ClusterExtensionRevision. + // Only Status updates will still propagated, but object changes will not be reconciled. + ClusterExtensionRevisionLifecycleStatePaused ClusterExtensionRevisionLifecycleState = "Paused" + // ClusterExtensionRevisionLifecycleStateArchived / "Archived" disables reconciliation while also "scaling to zero", + // which deletes all objects that are not excluded via the pausedFor property and + // removes itself from the owner list of all other objects previously under management. + ClusterExtensionRevisionLifecycleStateArchived ClusterExtensionRevisionLifecycleState = "Archived" +) + +type ClusterExtensionRevisionPhase struct { + Name string `json:"name"` + Objects []ClusterExtensionRevisionObject `json:"objects"` +} + +type ClusterExtensionRevisionObject struct { + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:pruning:PreserveUnknownFields + Object unstructured.Unstructured `json:"object"` +} + +type ClusterExtensionRevisionPrevious struct { + // +kubebuilder:validation:Required + Name string `json:"name"` + // +kubebuilder:validation:Required + UID types.UID `json:"uid"` +} + +// ClusterExtensionRevisionStatus defines the observed state of a ClusterExtensionRevision. +type ClusterExtensionRevisionStatus struct { + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:subresource:status + +// ClusterExtensionRevision is the Schema for the clusterextensionrevisions API +type ClusterExtensionRevision struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec is an optional field that defines the desired state of the ClusterExtension. + // +optional + Spec ClusterExtensionRevisionSpec `json:"spec,omitempty"` + + // status is an optional field that defines the observed state of the ClusterExtension. + // +optional + Status ClusterExtensionRevisionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterExtensionRevisionList contains a list of ClusterExtensionRevision +type ClusterExtensionRevisionList struct { + metav1.TypeMeta `json:",inline"` + + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // items is a required list of ClusterExtensionRevision objects. + // + // +kubebuilder:validation:Required + Items []ClusterExtensionRevision `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterExtensionRevision{}, &ClusterExtensionRevisionList{}) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 23fcf7d85..15a93701f 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -320,6 +320,167 @@ func (in *ClusterExtensionList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevision) DeepCopyInto(out *ClusterExtensionRevision) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevision. +func (in *ClusterExtensionRevision) DeepCopy() *ClusterExtensionRevision { + if in == nil { + return nil + } + out := new(ClusterExtensionRevision) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterExtensionRevision) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionList) DeepCopyInto(out *ClusterExtensionRevisionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterExtensionRevision, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionList. +func (in *ClusterExtensionRevisionList) DeepCopy() *ClusterExtensionRevisionList { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterExtensionRevisionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionObject) DeepCopyInto(out *ClusterExtensionRevisionObject) { + *out = *in + in.Object.DeepCopyInto(&out.Object) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionObject. +func (in *ClusterExtensionRevisionObject) DeepCopy() *ClusterExtensionRevisionObject { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionObject) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionPhase) DeepCopyInto(out *ClusterExtensionRevisionPhase) { + *out = *in + if in.Objects != nil { + in, out := &in.Objects, &out.Objects + *out = make([]ClusterExtensionRevisionObject, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionPhase. +func (in *ClusterExtensionRevisionPhase) DeepCopy() *ClusterExtensionRevisionPhase { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionPhase) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionPrevious) DeepCopyInto(out *ClusterExtensionRevisionPrevious) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionPrevious. +func (in *ClusterExtensionRevisionPrevious) DeepCopy() *ClusterExtensionRevisionPrevious { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionPrevious) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionSpec) DeepCopyInto(out *ClusterExtensionRevisionSpec) { + *out = *in + if in.Phases != nil { + in, out := &in.Phases, &out.Phases + *out = make([]ClusterExtensionRevisionPhase, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Previous != nil { + in, out := &in.Previous, &out.Previous + *out = make([]ClusterExtensionRevisionPrevious, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionSpec. +func (in *ClusterExtensionRevisionSpec) DeepCopy() *ClusterExtensionRevisionSpec { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionStatus) DeepCopyInto(out *ClusterExtensionRevisionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionStatus. +func (in *ClusterExtensionRevisionStatus) DeepCopy() *ClusterExtensionRevisionStatus { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterExtensionSpec) DeepCopyInto(out *ClusterExtensionSpec) { *out = *in diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index d426793d4..0f49d3335 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -32,13 +32,18 @@ import ( "github.com/spf13/cobra" rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + "k8s.io/apimachinery/pkg/labels" k8slabels "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" k8stypes "k8s.io/apimachinery/pkg/types" apimachineryrand "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/client-go/discovery" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/rest" "k8s.io/klog/v2" "k8s.io/utils/ptr" + "pkg.package-operator.run/boxcutter/managedcache" ctrl "sigs.k8s.io/controller-runtime" crcache "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/certwatcher" @@ -272,7 +277,8 @@ func run() error { "Metrics will not be served since the TLS certificate and key file are not provided.") } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + restConfig := ctrl.GetConfigOrDie() + mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ Scheme: scheme.Scheme, Metrics: metricsServerOptions, PprofBindAddress: cfg.pprofAddr, @@ -462,6 +468,31 @@ func run() error { return err } + // Boxcutter + discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig) + if err != nil { + setupLog.Error(err, "unable to create discovery client") + return err + } + mapFunc := func(ctx context.Context, ce *ocv1.ClusterExtension, c *rest.Config, o crcache.Options) (*rest.Config, crcache.Options, error) { + // TODO: Rest Config Mapping / change ServiceAccount + + // Cache scoping + req1, err := labels.NewRequirement( + controllers.ClusterExtensionRevisionOwnerLabel, selection.Equals, []string{ce.Name}) + if err != nil { + return nil, o, err + } + o.DefaultLabelSelector = labels.NewSelector().Add(*req1) + + return c, o, nil + } + accessManager := managedcache.NewObjectBoundAccessManager[*ocv1.ClusterExtension]( + ctrl.Log.WithName("accessmanager"), mapFunc, restConfig, crcache.Options{ + Scheme: mgr.GetScheme(), Mapper: mgr.GetRESTMapper(), + }) + // Boxcutter + if err = (&controllers.ClusterExtensionReconciler{ Client: cl, Resolver: resolver, @@ -476,6 +507,17 @@ func run() error { return err } + if err = (&controllers.ClusterExtensionRevisionReconciler{ + Client: cl, + AccessManager: accessManager, + Scheme: mgr.GetScheme(), + RestMapper: mgr.GetRESTMapper(), + DiscoveryClient: discoveryClient, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") + return err + } + if err = (&controllers.ClusterCatalogReconciler{ Client: cl, CatalogCache: catalogClientBackend, diff --git a/go.mod b/go.mod index 605549045..791ac10bf 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/cert-manager/cert-manager v1.18.2 github.com/containerd/containerd v1.7.27 github.com/containers/image/v5 v5.35.0 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/fsnotify/fsnotify v1.9.0 github.com/go-logr/logr v1.4.3 github.com/golang-jwt/jwt/v5 v5.2.2 @@ -41,6 +42,7 @@ require ( k8s.io/klog/v2 v2.130.1 k8s.io/kubernetes v1.33.2 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + pkg.package-operator.run/boxcutter v0.1.2 sigs.k8s.io/controller-runtime v0.21.0 sigs.k8s.io/controller-tools v0.18.0 sigs.k8s.io/crdify v0.4.1-0.20250613143457-398e4483fb58 @@ -87,7 +89,6 @@ require ( github.com/containers/storage v1.58.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v28.3.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect @@ -226,7 +227,7 @@ require ( go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - go.yaml.in/yaml/v3 v3.0.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.40.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect @@ -235,7 +236,7 @@ require ( golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect diff --git a/go.sum b/go.sum index c6c4f5c48..aa9c86254 100644 --- a/go.sum +++ b/go.sum @@ -571,8 +571,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -698,8 +698,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -781,6 +781,8 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +pkg.package-operator.run/boxcutter v0.1.2 h1:wtVxZ2cmA+fIfLUcfA8tRWhjq8MJ/52kwrYPjLwmEIo= +pkg.package-operator.run/boxcutter v0.1.2/go.mod h1:yVADyoqB8BqwubSArGBSk/WW+ZFUJgeZR+aHLHgrjbU= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFeieFlRqT627fVZ+tyfou/+S5S0H5ua0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= diff --git a/internal/operator-controller/applier/boxcutter.go b/internal/operator-controller/applier/boxcutter.go new file mode 100644 index 000000000..21e8cbf8b --- /dev/null +++ b/internal/operator-controller/applier/boxcutter.go @@ -0,0 +1,225 @@ +package applier + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "io/fs" + "slices" + "sort" + + "github.com/davecgh/go-spew/spew" + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + revisionHashAnnotation = "olm.operatorframework.io/hash" + revisionHistoryLimit = 5 +) + +type Boxcutter struct { + Client client.Client + Scheme *runtime.Scheme +} + +func (bc *Boxcutter) Apply( + ctx context.Context, contentFS fs.FS, + ext *ocv1.ClusterExtension, + objectLabels, storageLabels map[string]string, +) ([]client.Object, string, error) { + objs, err := bc.apply(ctx, contentFS, ext, objectLabels, storageLabels) + return objs, "", err +} + +func (bc *Boxcutter) apply( + ctx context.Context, contentFS fs.FS, + ext *ocv1.ClusterExtension, + objectLabels, _ map[string]string, +) ([]client.Object, error) { + reg, err := convert.ParseFS(contentFS) + if err != nil { + return nil, err + } + + watchNamespace, err := GetWatchNamespace(ext) + if err != nil { + return nil, err + } + + plain, err := convert.PlainConverter.Convert(reg, ext.Spec.Namespace, []string{watchNamespace}) + if err != nil { + return nil, err + } + + // objectLabels + objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain.Objects)) + for _, obj := range plain.Objects { + labels := obj.GetLabels() + if labels == nil { + labels = map[string]string{} + } + for k, v := range objectLabels { + labels[k] = v + } + obj.SetLabels(labels) + + gvk, err := apiutil.GVKForObject(obj, bc.Scheme) + if err != nil { + return nil, err + } + + unstrObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + unstr := unstructured.Unstructured{Object: unstrObj} + unstr.SetGroupVersionKind(gvk) + + objs = append(objs, ocv1.ClusterExtensionRevisionObject{ + Object: unstr, + }) + } + + // List all existing revisions + existingRevisionList := &ocv1.ClusterExtensionRevisionList{} + if err := bc.Client.List(ctx, existingRevisionList, client.MatchingLabels{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }); err != nil { + return nil, fmt.Errorf("listing revisions: %w", err) + } + sort.Sort(revisionAscending(existingRevisionList.Items)) + existingRevisions := existingRevisionList.Items + + // Build desired revision + desiredRevision := &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Revision: 1, + Phases: []ocv1.ClusterExtensionRevisionPhase{ + { + Name: "everything", + Objects: objs, + }, + }, + }, + } + desiredHash := computeSHA256Hash(desiredRevision.Spec.Phases) + + // Sort into current and previous revisions. + var ( + currentRevision *ocv1.ClusterExtensionRevision + prevRevisions []ocv1.ClusterExtensionRevision + ) + if len(existingRevisions) > 0 { + maybeCurrentRevision := existingRevisions[len(existingRevisions)-1] + + annotations := maybeCurrentRevision.GetAnnotations() + if annotations != nil { + if hash, ok := annotations[revisionHashAnnotation]; ok && + hash == desiredHash { + currentRevision = &maybeCurrentRevision + prevRevisions = existingRevisions[0 : len(existingRevisions)-1] // previous is everything excluding current + } + } + } + + if currentRevision == nil { + // all Revisions are outdated => create a new one. + prevRevisions = existingRevisions + revisionNumber := latestRevisionNumber(prevRevisions) + revisionNumber++ + + newRevision := desiredRevision + newRevision.Spec.Revision = revisionNumber + // newRevision.Spec.Previous + for _, prevRevision := range prevRevisions { + newRevision.Spec.Previous = append(newRevision.Spec.Previous, ocv1.ClusterExtensionRevisionPrevious{ + Name: prevRevision.Name, + UID: prevRevision.UID, + }) + } + + if err := controllerutil.SetControllerReference(ext, newRevision, bc.Scheme); err != nil { + return nil, fmt.Errorf("set ownerref: %w", err) + } + if err := bc.Client.Create(ctx, newRevision); err != nil { + return nil, fmt.Errorf("creating new Revision: %w", err) + } + } + + // Delete archived previous revisions over revisionHistory limit + numToDelete := len(prevRevisions) - revisionHistoryLimit + slices.Reverse(prevRevisions) + + for _, prevRev := range prevRevisions { + if numToDelete <= 0 { + break + } + + if err := client.IgnoreNotFound(bc.Client.Delete(ctx, &prevRev)); err != nil { + return nil, fmt.Errorf("failed to delete revision (history limit): %w", err) + } + numToDelete-- + } + + // TODO: Read status from revision. + + return plain.Objects, nil +} + +// computeSHA256Hash returns a sha236 hash value calculated from object. +func computeSHA256Hash(obj any) string { + hasher := sha256.New() + deepHashObject(hasher, obj) + return hex.EncodeToString(hasher.Sum(nil)) +} + +// deepHashObject writes specified object to hash using the spew library +// which follows pointers and prints actual values of the nested objects +// ensuring the hash does not change when a pointer changes. +func deepHashObject(hasher hash.Hash, objectToWrite any) { + hasher.Reset() + + printer := spew.ConfigState{ + Indent: " ", + SortKeys: true, + DisableMethods: true, + SpewKeys: true, + } + if _, err := printer.Fprintf(hasher, "%#v", objectToWrite); err != nil { + panic(err) + } +} + +type revisionAscending []ocv1.ClusterExtensionRevision + +func (a revisionAscending) Len() int { return len(a) } +func (a revisionAscending) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a revisionAscending) Less(i, j int) bool { + iObj := a[i] + jObj := a[j] + + return iObj.Spec.Revision < jObj.Spec.Revision +} + +func latestRevisionNumber(prevRevisions []ocv1.ClusterExtensionRevision) int64 { + if len(prevRevisions) == 0 { + return 0 + } + + return prevRevisions[len(prevRevisions)-1].Spec.Revision +} diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go new file mode 100644 index 000000000..f5057f556 --- /dev/null +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -0,0 +1,420 @@ +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/discovery" + "k8s.io/utils/ptr" + "pkg.package-operator.run/boxcutter" + "pkg.package-operator.run/boxcutter/machinery" + "pkg.package-operator.run/boxcutter/managedcache" + "pkg.package-operator.run/boxcutter/ownerhandling" + "pkg.package-operator.run/boxcutter/validation" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + ClusterExtensionRevisionOwnerLabel = "olm.operatorframework.io/owner" + + boxcutterSystemPrefixFieldOwner = "olm.operatorframework.io" + clusterExtensionRevisionTeardownFinalizer = "olm.operatorframework.io/teardown" +) + +// ClusterExtensionRevisionReconciler actions individual snapshots of ClusterExtensions, +// as part of the boxcutter integration. +type ClusterExtensionRevisionReconciler struct { + Client client.Client + AccessManager accessManager + Scheme *runtime.Scheme + RestMapper meta.RESTMapper + DiscoveryClient *discovery.DiscoveryClient +} + +type accessManager interface { + GetWithUser( + ctx context.Context, owner *ocv1.ClusterExtension, + user client.Object, usedFor []client.Object, + ) (managedcache.Accessor, error) + FreeWithUser(ctx context.Context, owner *ocv1.ClusterExtension, user client.Object) error + Source(handler.EventHandler, ...predicate.Predicate) source.Source +} + +func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { + l := log.FromContext(ctx).WithName("cluster-extension-revision") + ctx = log.IntoContext(ctx, l) + + rev := &ocv1.ClusterExtensionRevision{} + if err := c.Client.Get( + ctx, req.NamespacedName, rev); err != nil { + return res, client.IgnoreNotFound(err) + } + + l = l.WithValues("key", req.NamespacedName.String()) + l.Info("reconcile starting") + defer l.Info("reconcile ending") + + controller, ok := getControllingClusterExtension(rev) + if !ok { + // ClusterExtension revisions can't exist without a ClusterExtension in control. + // This situation can only appear if the ClusterExtension object has been deleted with --cascade=Orphan. + // To not leave unactionable resources on the cluster, we are going to just + // reap the revision reverences and propagate the Orphan deletion. + err := client.IgnoreNotFound( + c.Client.Delete(ctx, rev, client.PropagationPolicy(metav1.DeletePropagationOrphan), client.Preconditions{ + UID: ptr.To(rev.GetUID()), + ResourceVersion: ptr.To(rev.GetResourceVersion()), + }), + ) + if err != nil { + return res, err + } + + if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { + return res, err + } + return res, nil + } + + return c.reconcile(ctx, controller, rev) +} + +func (c *ClusterExtensionRevisionReconciler) reconcile( + ctx context.Context, ce *ocv1.ClusterExtension, rev *ocv1.ClusterExtensionRevision, +) (res ctrl.Result, err error) { + l := log.FromContext(ctx) + + revision, opts, previous, err := toBoxcutterRevision(ce.Name, rev) + if err != nil { + return res, fmt.Errorf("converting CM to revision: %w", err) + } + + var objects []client.Object + for _, phase := range revision.GetPhases() { + for _, pobj := range phase.GetObjects() { + objects = append(objects, &pobj) + } + } + accessor, err := c.AccessManager.GetWithUser(ctx, ce, rev, objects) + if err != nil { + return res, fmt.Errorf("get cache: %w", err) + } + + // Boxcutter machinery setup. + os := ownerhandling.NewNative(c.Scheme) + pval := validation.NewClusterPhaseValidator(c.RestMapper, accessor) + rval := validation.NewRevisionValidator() + comp := machinery.NewComparator( + os, c.DiscoveryClient, c.Scheme, boxcutterSystemPrefixFieldOwner) + oe := machinery.NewObjectEngine( + c.Scheme, accessor, accessor, + os, comp, boxcutterSystemPrefixFieldOwner, boxcutterSystemPrefixFieldOwner, + ) + pe := machinery.NewPhaseEngine(oe, pval) + re := machinery.NewRevisionEngine(pe, rval, accessor) + + if !rev.DeletionTimestamp.IsZero() || + rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived { + // + // Teardown + // + tres, err := re.Teardown(ctx, *revision) + if err != nil { + return res, fmt.Errorf("revision teardown: %w", err) + } + + l.Info("teardown report", "report", tres.String()) + + if !tres.IsComplete() { + return res, nil + } + if err := c.AccessManager.FreeWithUser(ctx, ce, rev); err != nil { + return res, fmt.Errorf("get cache: %w", err) + } + if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { + return res, err + } + return res, nil + } + + // + // Reconcile + // + if err := c.ensureFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { + return res, err + } + rres, err := re.Reconcile(ctx, *revision, opts...) + if err != nil { + return res, fmt.Errorf("revision reconcile: %w", err) + } + l.Info("reconcile report", "report", rres.String()) + + // Retry failing preflight checks with a flat 10s retry. + // TODO: report status, backoff? + if verr := rres.GetValidationError(); verr != nil { + l.Info("preflight error, retrying after 10s", "err", verr.String()) + + res.RequeueAfter = 10 * time.Second + //nolint:nilerr + return res, nil + } + for _, pres := range rres.GetPhases() { + if verr := pres.GetValidationError(); verr != nil { + l.Info("preflight error, retrying after 10s", "err", verr.String()) + + res.RequeueAfter = 10 * time.Second + //nolint:nilerr + return res, nil + } + } + + if rres.IsComplete() { + // Archive other revisions. + for _, a := range previous { + if err := c.Client.Patch(ctx, a, client.RawPatch( + types.MergePatchType, []byte(`{"data":{"state":"Archived"}}`))); err != nil { + return res, fmt.Errorf("archive previous Revision: %w", err) + } + } + + // Report status. + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: "Available", + Status: metav1.ConditionTrue, + Reason: "Available", + Message: "Object is available and passes all probes.", + ObservedGeneration: rev.Generation, + }) + if !meta.IsStatusConditionTrue(rev.Status.Conditions, "Succeeded") { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: "Succeeded", + Status: metav1.ConditionTrue, + Reason: "RolloutSuccess", + Message: "Revision succeeded rolling out.", + ObservedGeneration: rev.Generation, + }) + } + } else { + var probeFailureMsgs []string + for _, pres := range rres.GetPhases() { + if pres.IsComplete() { + continue + } + for _, ores := range pres.GetObjects() { + pr := ores.Probes()[boxcutter.ProgressProbeType] + if pr.Success { + continue + } + + obj := ores.Object() + gvk := obj.GetObjectKind().GroupVersionKind() + probeFailureMsgs = append(probeFailureMsgs, fmt.Sprintf( + "Object %s.%s %s/%s: %v", + gvk.Kind, gvk.GroupVersion().String(), + obj.GetNamespace(), obj.GetName(), strings.Join(pr.Messages, " and "), + )) + break + } + } + if len(probeFailureMsgs) > 0 { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: "Available", + Status: metav1.ConditionFalse, + Reason: "ProbeFailure", + Message: strings.Join(probeFailureMsgs, "\n"), + ObservedGeneration: rev.Generation, + }) + } else { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: "Available", + Status: metav1.ConditionFalse, + Reason: "Incomplete", + Message: "Revision has not been rolled out completely.", + ObservedGeneration: rev.Generation, + }) + } + } + if rres.InTransistion() { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: "InTransition", + Status: metav1.ConditionTrue, + Reason: "InTransition", + Message: "Rollout in progress.", + ObservedGeneration: rev.Generation, + }) + } else { + meta.RemoveStatusCondition(&rev.Status.Conditions, "InTransition") + } + + return res, c.Client.Status().Update(ctx, rev) +} + +func (c *ClusterExtensionRevisionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For( + &corev1.ConfigMap{}, + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + WatchesRawSource( + c.AccessManager.Source( + handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &ocv1.ClusterExtension{}), + predicate.ResourceVersionChangedPredicate{}, + ), + ). + Complete(c) +} + +func (c *ClusterExtensionRevisionReconciler) ensureFinalizer( + ctx context.Context, obj client.Object, finalizer string, +) error { + if controllerutil.ContainsFinalizer(obj, finalizer) { + return nil + } + + controllerutil.AddFinalizer(obj, finalizer) + patch := map[string]any{ + "metadata": map[string]any{ + "resourceVersion": obj.GetResourceVersion(), + "finalizers": obj.GetFinalizers(), + }, + } + patchJSON, err := json.Marshal(patch) + if err != nil { + return fmt.Errorf("marshalling patch to remove finalizer: %w", err) + } + if err := c.Client.Patch(ctx, obj, client.RawPatch(types.MergePatchType, patchJSON)); err != nil { + return fmt.Errorf("adding finalizer: %w", err) + } + return nil +} + +func (c *ClusterExtensionRevisionReconciler) removeFinalizer( + ctx context.Context, obj client.Object, finalizer string, +) error { + if !controllerutil.ContainsFinalizer(obj, finalizer) { + return nil + } + + controllerutil.RemoveFinalizer(obj, finalizer) + + patch := map[string]any{ + "metadata": map[string]any{ + "resourceVersion": obj.GetResourceVersion(), + "finalizers": obj.GetFinalizers(), + }, + } + patchJSON, err := json.Marshal(patch) + if err != nil { + return fmt.Errorf("marshalling patch to remove finalizer: %w", err) + } + if err := c.Client.Patch(ctx, obj, client.RawPatch(types.MergePatchType, patchJSON)); err != nil { + return fmt.Errorf("removing finalizer: %w", err) + } + return nil +} + +// getControllingClusterExtension checks the objects ownerreferences for a ClusterExtension +// with the controller flag set to true. +// Returns a ClusterExtension with metadata recovered from the OwnerRef or nil. +func getControllingClusterExtension(obj client.Object) (*ocv1.ClusterExtension, bool) { + for _, v := range obj.GetOwnerReferences() { + if v.Controller != nil && *v.Controller && + v.APIVersion == ocv1.GroupVersion.String() && + v.Kind == "ClusterExtension" { + return &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + UID: v.UID, + Name: v.Name, + }, + }, true + } + } + return nil, false +} + +func toBoxcutterRevision(clusterExtensionName string, rev *ocv1.ClusterExtensionRevision) ( + r *boxcutter.Revision, opts []boxcutter.RevisionReconcileOption, previous []client.Object, err error, +) { + r = &boxcutter.Revision{ + Name: rev.Name, + Owner: rev, + Revision: rev.Spec.Revision, + } + for _, specPhase := range rev.Spec.Phases { + phase := boxcutter.Phase{Name: specPhase.Name} + for _, specObj := range specPhase.Objects { + obj := specObj.Object + + labels := obj.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[ClusterExtensionRevisionOwnerLabel] = clusterExtensionName + obj.SetLabels(labels) + + phase.Objects = append(phase.Objects, obj) + } + r.Phases = append(r.Phases, phase) + } + + for _, specPrevious := range rev.Spec.Previous { + prev := &unstructured.Unstructured{} + prev.SetName(specPrevious.Name) + prev.SetUID(specPrevious.UID) + prev.SetGroupVersionKind(ocv1.GroupVersion.WithKind(ocv1.ClusterExtensionRevisionKind)) + previous = append(previous, prev) + } + + opts = []boxcutter.RevisionReconcileOption{ + boxcutter.WithPreviousOwners(previous), + boxcutter.WithProbe(boxcutter.ProgressProbeType, boxcutter.ProbeFunc(func(obj client.Object) (success bool, messages []string) { + deployGK := schema.GroupKind{ + Group: "apps", Kind: "Deployment", + } + if obj.GetObjectKind().GroupVersionKind().GroupKind() != deployGK { + return true, nil + } + ustrObj := obj.(*unstructured.Unstructured) + depl := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(ustrObj.Object, depl); err != nil { + return false, []string{err.Error()} + } + + if depl.Status.ObservedGeneration != depl.Generation { + return false, []string{".status.observedGeneration outdated"} + } + for _, cond := range depl.Status.Conditions { + if cond.Type == "Available" && + cond.Status == corev1.ConditionTrue && + depl.Status.UpdatedReplicas == *depl.Spec.Replicas { + return true, nil + } + } + + return false, []string{"not available or not fully updated"} + })), + } + if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStatePaused { + opts = append(opts, boxcutter.WithPaused{}) + } + return +} From 10dc3124a914e274fda8ce8721dc5c17483884e8 Mon Sep 17 00:00:00 2001 From: Nico Schieder Date: Wed, 30 Apr 2025 17:54:49 +0200 Subject: [PATCH 02/10] Connect Boxcutter Applier with ClusterExtension --- api/v1/clusterextensionrevision_types.go | 7 +- api/v1/zz_generated.deepcopy.go | 64 ++++++++++++ cmd/operator-controller/main.go | 98 +++++++++++-------- .../base/operator-controller/rbac/role.yaml | 16 ++- .../operator-controller/applier/boxcutter.go | 22 +++-- .../clusterextension_controller.go | 1 + .../clusterextensionrevision_controller.go | 41 +++++--- scripts/install.tpl.sh | 8 +- 8 files changed, 187 insertions(+), 70 deletions(-) diff --git a/api/v1/clusterextensionrevision_types.go b/api/v1/clusterextensionrevision_types.go index 1322f9874..66382730c 100644 --- a/api/v1/clusterextensionrevision_types.go +++ b/api/v1/clusterextensionrevision_types.go @@ -29,16 +29,16 @@ type ClusterExtensionRevisionSpec struct { // Specifies the lifecycle state of the ClusterExtensionRevision. // +kubebuilder:default="Active" // +kubebuilder:validation:Enum=Active;Paused;Archived - // +kubebuilder:validation:XValidation:rule="oldSelf == "Active" || oldSelf == "Paused" || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive" + // +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive" LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"` // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" Revision int64 `json:"revision"` // +kubebuilder:validation:Required - // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="phases is immutable" + // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="phases is immutable" Phases []ClusterExtensionRevisionPhase `json:"phases"` // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable" - Previous []ClusterExtensionRevisionPrevious `json:"previous"` + Previous []ClusterExtensionRevisionPrevious `json:"previous,omitempty"` } // ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision. @@ -59,6 +59,7 @@ const ( type ClusterExtensionRevisionPhase struct { Name string `json:"name"` Objects []ClusterExtensionRevisionObject `json:"objects"` + Slices []string `json:"slices,omitempty"` } type ClusterExtensionRevisionObject struct { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 15a93701f..7e657025e 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -530,6 +530,70 @@ func (in *ClusterExtensionStatus) DeepCopy() *ClusterExtensionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectSlice) DeepCopyInto(out *ClusterObjectSlice) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Objects != nil { + in, out := &in.Objects, &out.Objects + *out = make([]ClusterExtensionRevisionObject, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectSlice. +func (in *ClusterObjectSlice) DeepCopy() *ClusterObjectSlice { + if in == nil { + return nil + } + out := new(ClusterObjectSlice) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterObjectSlice) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectSliceList) DeepCopyInto(out *ClusterObjectSliceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterObjectSlice, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectSliceList. +func (in *ClusterObjectSliceList) DeepCopy() *ClusterObjectSliceList { + if in == nil { + return nil + } + out := new(ClusterObjectSliceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterObjectSliceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImageSource) DeepCopyInto(out *ImageSource) { *out = *in diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 0f49d3335..7281d85a0 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -31,7 +31,6 @@ import ( "github.com/containers/image/v5/types" "github.com/spf13/cobra" rbacv1 "k8s.io/api/rbac/v1" - apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" "k8s.io/apimachinery/pkg/labels" k8slabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" @@ -60,7 +59,6 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/action" "github.com/operator-framework/operator-controller/internal/operator-controller/applier" "github.com/operator-framework/operator-controller/internal/operator-controller/authentication" - "github.com/operator-framework/operator-controller/internal/operator-controller/authorization" "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/cache" catalogclient "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/client" "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager" @@ -68,11 +66,6 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/features" "github.com/operator-framework/operator-controller/internal/operator-controller/finalizers" "github.com/operator-framework/operator-controller/internal/operator-controller/resolve" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/certproviders" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" @@ -417,45 +410,50 @@ func run() error { }, } - aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig()) - if err != nil { - setupLog.Error(err, "unable to create apiextensions client") - return err - } + // aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig()) + // if err != nil { + // setupLog.Error(err, "unable to create apiextensions client") + // return err + // } - preflights := []applier.Preflight{ - crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()), - } + // preflights := []applier.Preflight{ + // crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()), + // } - // determine if PreAuthorizer should be enabled based on feature gate - var preAuth authorization.PreAuthorizer - if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { - preAuth = authorization.NewRBACPreAuthorizer(mgr.GetClient()) + // // determine if PreAuthorizer should be enabled based on feature gate + // var preAuth authorization.PreAuthorizer + // if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { + // preAuth = authorization.NewRBACPreAuthorizer(mgr.GetClient()) + // } + + boxcutterApplier := &applier.Boxcutter{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), } // determine if a certificate provider should be set in the bundle renderer and feature support for the provider // based on the feature flag - var certProvider render.CertificateProvider - var isWebhookSupportEnabled bool - if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderCertManager) { - certProvider = certproviders.CertManagerCertificateProvider{} - isWebhookSupportEnabled = true - } else if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderOpenshiftServiceCA) { - certProvider = certproviders.OpenshiftServiceCaCertificateProvider{} - isWebhookSupportEnabled = true - } + // var certProvider render.CertificateProvider + // var isWebhookSupportEnabled bool + // if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderCertManager) { + // certProvider = certproviders.CertManagerCertificateProvider{} + // isWebhookSupportEnabled = true + // } else if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderOpenshiftServiceCA) { + // certProvider = certproviders.OpenshiftServiceCaCertificateProvider{} + // isWebhookSupportEnabled = true + // } // now initialize the helmApplier, assigning the potentially nil preAuth - helmApplier := &applier.Helm{ - ActionClientGetter: acg, - Preflights: preflights, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{ - BundleRenderer: registryv1.Renderer, - CertificateProvider: certProvider, - IsWebhookSupportEnabled: isWebhookSupportEnabled, - }, - PreAuthorizer: preAuth, - } + // helmApplier := &applier.Helm{ + // ActionClientGetter: acg, + // Preflights: preflights, + // BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{ + // BundleRenderer: registryv1.Renderer, + // CertificateProvider: certProvider, + // IsWebhookSupportEnabled: isWebhookSupportEnabled, + // }, + // PreAuthorizer: preAuth, + // } cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper()) err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { @@ -475,7 +473,18 @@ func run() error { return err } mapFunc := func(ctx context.Context, ce *ocv1.ClusterExtension, c *rest.Config, o crcache.Options) (*rest.Config, crcache.Options, error) { - // TODO: Rest Config Mapping / change ServiceAccount + saKey := client.ObjectKey{ + Name: ce.Spec.ServiceAccount.Name, + Namespace: ce.Spec.Namespace, + } + saConfig := rest.AnonymousClientConfig(c) + saConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper { + return &authentication.TokenInjectingRoundTripper{ + Tripper: rt, + TokenGetter: tokenGetter, + Key: saKey, + } + }) // Cache scoping req1, err := labels.NewRequirement( @@ -485,12 +494,17 @@ func run() error { } o.DefaultLabelSelector = labels.NewSelector().Add(*req1) - return c, o, nil + return saConfig, o, nil } - accessManager := managedcache.NewObjectBoundAccessManager[*ocv1.ClusterExtension]( + + accessManager := managedcache.NewObjectBoundAccessManager( ctrl.Log.WithName("accessmanager"), mapFunc, restConfig, crcache.Options{ Scheme: mgr.GetScheme(), Mapper: mgr.GetRESTMapper(), }) + if err := mgr.Add(accessManager); err != nil { + setupLog.Error(err, "unable to register AccessManager") + return err + } // Boxcutter if err = (&controllers.ClusterExtensionReconciler{ @@ -498,7 +512,7 @@ func run() error { Resolver: resolver, ImageCache: imageCache, ImagePuller: imagePuller, - Applier: helmApplier, + Applier: boxcutterApplier, InstalledBundleGetter: &controllers.DefaultInstalledBundleGetter{ActionClientGetter: acg}, Finalizers: clusterExtensionFinalizers, Manager: cm, diff --git a/config/base/operator-controller/rbac/role.yaml b/config/base/operator-controller/rbac/role.yaml index d18eb4c6c..c48bb0c6e 100644 --- a/config/base/operator-controller/rbac/role.yaml +++ b/config/base/operator-controller/rbac/role.yaml @@ -27,8 +27,10 @@ rules: - apiGroups: - olm.operatorframework.io resources: - - clusterextensions + - clusterextensionrevisions verbs: + - create + - delete - get - list - patch @@ -37,16 +39,28 @@ rules: - apiGroups: - olm.operatorframework.io resources: + - clusterextensionrevisions/finalizers - clusterextensions/finalizers verbs: - update - apiGroups: - olm.operatorframework.io resources: + - clusterextensionrevisions/status - clusterextensions/status verbs: - patch - update +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: diff --git a/internal/operator-controller/applier/boxcutter.go b/internal/operator-controller/applier/boxcutter.go index 21e8cbf8b..f2b1715bf 100644 --- a/internal/operator-controller/applier/boxcutter.go +++ b/internal/operator-controller/applier/boxcutter.go @@ -13,7 +13,8 @@ import ( "github.com/davecgh/go-spew/spew" ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -28,8 +29,9 @@ const ( ) type Boxcutter struct { - Client client.Client - Scheme *runtime.Scheme + Client client.Client + Scheme *runtime.Scheme + BundleRenderer render.BundleRenderer } func (bc *Boxcutter) Apply( @@ -46,7 +48,7 @@ func (bc *Boxcutter) apply( ext *ocv1.ClusterExtension, objectLabels, _ map[string]string, ) ([]client.Object, error) { - reg, err := convert.ParseFS(contentFS) + reg, err := source.FromFS(contentFS).GetBundle() if err != nil { return nil, err } @@ -56,14 +58,14 @@ func (bc *Boxcutter) apply( return nil, err } - plain, err := convert.PlainConverter.Convert(reg, ext.Spec.Namespace, []string{watchNamespace}) + plain, err := bc.BundleRenderer.Render(reg, ext.Spec.Namespace, render.WithTargetNamespaces(watchNamespace)) if err != nil { return nil, err } // objectLabels - objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain.Objects)) - for _, obj := range plain.Objects { + objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain)) + for _, obj := range plain { labels := obj.GetLabels() if labels == nil { labels = map[string]string{} @@ -103,6 +105,7 @@ func (bc *Boxcutter) apply( // Build desired revision desiredRevision := &ocv1.ClusterExtensionRevision{ ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, Labels: map[string]string{ controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, }, @@ -144,8 +147,9 @@ func (bc *Boxcutter) apply( revisionNumber++ newRevision := desiredRevision + newRevision.Name = fmt.Sprintf("%s-%d", ext.Name, revisionNumber) + newRevision.Annotations[revisionHashAnnotation] = desiredHash newRevision.Spec.Revision = revisionNumber - // newRevision.Spec.Previous for _, prevRevision := range prevRevisions { newRevision.Spec.Previous = append(newRevision.Spec.Previous, ocv1.ClusterExtensionRevisionPrevious{ Name: prevRevision.Name, @@ -178,7 +182,7 @@ func (bc *Boxcutter) apply( // TODO: Read status from revision. - return plain.Objects, nil + return plain, nil } // computeSHA256Hash returns a sha236 hash value calculated from object. diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index 5b180d9cc..fdac3fbf1 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -418,6 +418,7 @@ func (r *ClusterExtensionReconciler) SetupWithManager(mgr ctrl.Manager) error { controller, err := ctrl.NewControllerManagedBy(mgr). For(&ocv1.ClusterExtension{}). Named("controller-operator-cluster-extension-controller"). + Owns(&ocv1.ClusterExtensionRevision{}). Watches(&ocv1.ClusterCatalog{}, crhandler.EnqueueRequestsFromMapFunc(clusterExtensionRequestsForCatalog(mgr.GetClient(), mgr.GetLogger())), builder.WithPredicates(predicate.Funcs{ diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go index f5057f556..29d18f17d 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -59,6 +59,10 @@ type accessManager interface { Source(handler.EventHandler, ...predicate.Predicate) source.Source } +//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions,verbs=get;list;watch;update;patch;create;delete +//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions/status,verbs=update;patch +//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions/finalizers,verbs=update + func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { l := log.FromContext(ctx).WithName("cluster-extension-revision") ctx = log.IntoContext(ctx, l) @@ -79,16 +83,19 @@ func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req // This situation can only appear if the ClusterExtension object has been deleted with --cascade=Orphan. // To not leave unactionable resources on the cluster, we are going to just // reap the revision reverences and propagate the Orphan deletion. - err := client.IgnoreNotFound( - c.Client.Delete(ctx, rev, client.PropagationPolicy(metav1.DeletePropagationOrphan), client.Preconditions{ - UID: ptr.To(rev.GetUID()), - ResourceVersion: ptr.To(rev.GetResourceVersion()), - }), - ) - if err != nil { - return res, err + if rev.DeletionTimestamp.IsZero() { + err := client.IgnoreNotFound( + c.Client.Delete(ctx, rev, client.PropagationPolicy(metav1.DeletePropagationOrphan), client.Preconditions{ + UID: ptr.To(rev.GetUID()), + ResourceVersion: ptr.To(rev.GetResourceVersion()), + }), + ) + if err != nil { + return res, err + } + // we get requeued to remove the finalizer. + return res, nil } - if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { return res, err } @@ -114,6 +121,18 @@ func (c *ClusterExtensionRevisionReconciler) reconcile( objects = append(objects, &pobj) } } + + // THIS IS STUPID, PLEASE FIX! + // Revisions need individual finalizers on the ClusterExtension to prevent it's premature deletion. + if rev.DeletionTimestamp.IsZero() && + rev.Spec.LifecycleState != ocv1.ClusterExtensionRevisionLifecycleStateArchived { + // We can't lookup the complete ClusterExtension when it's already deleted. + // This only works when the controller-manager is not restarted during teardown. + if err := c.Client.Get(ctx, client.ObjectKeyFromObject(ce), ce); err != nil { + return res, err + } + } + accessor, err := c.AccessManager.GetWithUser(ctx, ce, rev, objects) if err != nil { return res, fmt.Errorf("get cache: %w", err) @@ -271,12 +290,12 @@ func (c *ClusterExtensionRevisionReconciler) reconcile( func (c *ClusterExtensionRevisionReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For( - &corev1.ConfigMap{}, + &ocv1.ClusterExtensionRevision{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). WatchesRawSource( c.AccessManager.Source( - handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &ocv1.ClusterExtension{}), + handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &ocv1.ClusterExtensionRevision{}), predicate.ResourceVersionChangedPredicate{}, ), ). diff --git a/scripts/install.tpl.sh b/scripts/install.tpl.sh index 8088a2515..313d8c564 100644 --- a/scripts/install.tpl.sh +++ b/scripts/install.tpl.sh @@ -63,9 +63,9 @@ kubectl_wait_for_query() { kubectl apply -f "https://github.com/cert-manager/cert-manager/releases/download/${cert_mgr_version}/cert-manager.yaml" # Wait for cert-manager to be fully ready -kubectl_wait "cert-manager" "deployment/cert-manager-webhook" "60s" -kubectl_wait "cert-manager" "deployment/cert-manager-cainjector" "60s" -kubectl_wait "cert-manager" "deployment/cert-manager" "60s" +kubectl_wait "cert-manager" "deployment/cert-manager-webhook" "6000s" +kubectl_wait "cert-manager" "deployment/cert-manager-cainjector" "6000s" +kubectl_wait "cert-manager" "deployment/cert-manager" "6000s" kubectl_wait_for_query "mutatingwebhookconfigurations/cert-manager-webhook" '{.webhooks[0].clientConfig.caBundle}' 60 5 kubectl_wait_for_query "validatingwebhookconfigurations/cert-manager-webhook" '{.webhooks[0].clientConfig.caBundle}' 60 5 @@ -77,5 +77,5 @@ kubectl_wait "olmv1-system" "deployment/operator-controller-controller-manager" if [[ "${install_default_catalogs}" != "false" ]]; then kubectl apply -f "${default_catalogs_manifest}" - kubectl wait --for=condition=Serving "clustercatalog/operatorhubio" --timeout="60s" + kubectl wait --for=condition=Serving "clustercatalog/operatorhubio" --timeout="6000s" fi From 87b9da562f114b24a77e92ac65cdde52539f80ce Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 15 Jul 2025 09:00:34 +0200 Subject: [PATCH 03/10] Add Boxcutter featuregate Signed-off-by: Per Goncalves da Silva --- cmd/operator-controller/main.go | 106 ++++++++++-------- .../boxcutter-runtime/kustomization.yaml | 9 ++ .../patches/enable-featuregate.yaml | 4 + .../clusterextension_controller.go | 22 +++- .../operator-controller/features/features.go | 8 ++ 5 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 config/components/features/boxcutter-runtime/kustomization.yaml create mode 100644 config/components/features/boxcutter-runtime/patches/enable-featuregate.yaml diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 7281d85a0..c3da78e2f 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -22,6 +22,13 @@ import ( "errors" "flag" "fmt" + "github.com/operator-framework/operator-controller/internal/operator-controller/authorization" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/certproviders" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" + apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" "net/http" "os" "path/filepath" @@ -410,50 +417,47 @@ func run() error { }, } - // aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig()) - // if err != nil { - // setupLog.Error(err, "unable to create apiextensions client") - // return err - // } - - // preflights := []applier.Preflight{ - // crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()), - // } - - // // determine if PreAuthorizer should be enabled based on feature gate - // var preAuth authorization.PreAuthorizer - // if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { - // preAuth = authorization.NewRBACPreAuthorizer(mgr.GetClient()) - // } - - boxcutterApplier := &applier.Boxcutter{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - } - - // determine if a certificate provider should be set in the bundle renderer and feature support for the provider - // based on the feature flag - // var certProvider render.CertificateProvider - // var isWebhookSupportEnabled bool - // if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderCertManager) { - // certProvider = certproviders.CertManagerCertificateProvider{} - // isWebhookSupportEnabled = true - // } else if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderOpenshiftServiceCA) { - // certProvider = certproviders.OpenshiftServiceCaCertificateProvider{} - // isWebhookSupportEnabled = true - // } - - // now initialize the helmApplier, assigning the potentially nil preAuth - // helmApplier := &applier.Helm{ - // ActionClientGetter: acg, - // Preflights: preflights, - // BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{ - // BundleRenderer: registryv1.Renderer, - // CertificateProvider: certProvider, - // IsWebhookSupportEnabled: isWebhookSupportEnabled, - // }, - // PreAuthorizer: preAuth, - // } + aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig()) + if err != nil { + setupLog.Error(err, "unable to create apiextensions client") + return err + } + + preflights := []applier.Preflight{ + crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()), + } + + // determine if PreAuthorizer should be enabled based on feature gate + var preAuth authorization.PreAuthorizer + if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { + preAuth = authorization.NewRBACPreAuthorizer(mgr.GetClient()) + } + + // create applier + var ctrlBuilderOpts []controllers.ControllerBuilderOption + var extApplier controllers.Applier + + if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) { + // TODO: add support for preflight checks + extApplier = &applier.Boxcutter{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + } + ctrlBuilderOpts = append(ctrlBuilderOpts, controllers.WithOwns(&ocv1.ClusterExtensionRevision{})) + } else { + // now initialize the helmApplier, assigning the potentially nil preAuth + certProvider := getCertificateProvider() + extApplier = &applier.Helm{ + ActionClientGetter: acg, + Preflights: preflights, + BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{ + BundleRenderer: registryv1.Renderer, + CertificateProvider: certProvider, + IsWebhookSupportEnabled: certProvider != nil, + }, + PreAuthorizer: preAuth, + } + } cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper()) err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { @@ -505,18 +509,17 @@ func run() error { setupLog.Error(err, "unable to register AccessManager") return err } - // Boxcutter if err = (&controllers.ClusterExtensionReconciler{ Client: cl, Resolver: resolver, ImageCache: imageCache, ImagePuller: imagePuller, - Applier: boxcutterApplier, + Applier: extApplier, InstalledBundleGetter: &controllers.DefaultInstalledBundleGetter{ActionClientGetter: acg}, Finalizers: clusterExtensionFinalizers, Manager: cm, - }).SetupWithManager(mgr); err != nil { + }).SetupWithManager(mgr, ctrlBuilderOpts...); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") return err } @@ -577,6 +580,15 @@ func run() error { return nil } +func getCertificateProvider() render.CertificateProvider { + if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderCertManager) { + return certproviders.CertManagerCertificateProvider{} + } else if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderOpenshiftServiceCA) { + return certproviders.OpenshiftServiceCaCertificateProvider{} + } + return nil +} + func main() { if err := operatorControllerCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/config/components/features/boxcutter-runtime/kustomization.yaml b/config/components/features/boxcutter-runtime/kustomization.yaml new file mode 100644 index 000000000..d075a1121 --- /dev/null +++ b/config/components/features/boxcutter-runtime/kustomization.yaml @@ -0,0 +1,9 @@ +# DO NOT ADD A NAMESPACE HERE +--- +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - target: + kind: Deployment + name: operator-controller-controller-manager + path: patches/enable-featuregate.yaml diff --git a/config/components/features/boxcutter-runtime/patches/enable-featuregate.yaml b/config/components/features/boxcutter-runtime/patches/enable-featuregate.yaml new file mode 100644 index 000000000..97f8b89be --- /dev/null +++ b/config/components/features/boxcutter-runtime/patches/enable-featuregate.yaml @@ -0,0 +1,4 @@ +# enable Boxcutter runtime feature gate +- op: add + path: /spec/template/spec/containers/0/args/- + value: "--feature-gates=BoxcutterRuntime=true" diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index fdac3fbf1..088cdd125 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -413,12 +413,19 @@ func SetDeprecationStatus(ext *ocv1.ClusterExtension, bundleName string, depreca } } +type ControllerBuilderOption func(builder *ctrl.Builder) + +func WithOwns(obj client.Object) ControllerBuilderOption { + return func(builder *ctrl.Builder) { + builder.Owns(obj) + } +} + // SetupWithManager sets up the controller with the Manager. -func (r *ClusterExtensionReconciler) SetupWithManager(mgr ctrl.Manager) error { - controller, err := ctrl.NewControllerManagedBy(mgr). +func (r *ClusterExtensionReconciler) SetupWithManager(mgr ctrl.Manager, opts ...ControllerBuilderOption) error { + ctrlBuilder := ctrl.NewControllerManagedBy(mgr). For(&ocv1.ClusterExtension{}). Named("controller-operator-cluster-extension-controller"). - Owns(&ocv1.ClusterExtensionRevision{}). Watches(&ocv1.ClusterCatalog{}, crhandler.EnqueueRequestsFromMapFunc(clusterExtensionRequestsForCatalog(mgr.GetClient(), mgr.GetLogger())), builder.WithPredicates(predicate.Funcs{ @@ -437,8 +444,13 @@ func (r *ClusterExtensionReconciler) SetupWithManager(mgr ctrl.Manager) error { } return true }, - })). - Build(r) + })) + + for _, applyOpt := range opts { + applyOpt(ctrlBuilder) + } + + controller, err := ctrlBuilder.Build(r) if err != nil { return err } diff --git a/internal/operator-controller/features/features.go b/internal/operator-controller/features/features.go index 41bad3cf7..1abdf0a18 100644 --- a/internal/operator-controller/features/features.go +++ b/internal/operator-controller/features/features.go @@ -17,6 +17,7 @@ const ( WebhookProviderCertManager featuregate.Feature = "WebhookProviderCertManager" WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA" HelmChartSupport featuregate.Feature = "HelmChartSupport" + BoxcutterRuntime featuregate.Feature = "BoxcutterRuntime" ) var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ @@ -72,6 +73,13 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature PreRelease: featuregate.Alpha, LockToDefault: false, }, + + // BoxcutterRuntime configures OLM to use the Boxcutter runtime for extension lifecycling + BoxcutterRuntime: { + Default: false, + PreRelease: featuregate.Alpha, + LockToDefault: false, + }, } var OperatorControllerFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate() From f169bb426367e73e459f4ceb2fc6b4bbcf026aa0 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 15 Jul 2025 12:18:37 +0200 Subject: [PATCH 04/10] Update experimental manifest and code generation Signed-off-by: Per Goncalves da Silva --- Makefile | 7 +- .../rbac/experimental/kustomization.yaml | 2 + .../rbac/{ => experimental}/role.yaml | 0 .../rbac/kustomization.yaml | 2 +- .../rbac/standard/kustomization.yaml | 2 + .../rbac/standard/role.yaml | 87 +++++++++++++++++++ hack/tools/update-crds.sh | 8 +- 7 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 config/base/operator-controller/rbac/experimental/kustomization.yaml rename config/base/operator-controller/rbac/{ => experimental}/role.yaml (100%) create mode 100644 config/base/operator-controller/rbac/standard/kustomization.yaml create mode 100644 config/base/operator-controller/rbac/standard/role.yaml diff --git a/Makefile b/Makefile index fef152f72..535f69bb8 100644 --- a/Makefile +++ b/Makefile @@ -150,7 +150,8 @@ manifests: $(CONTROLLER_GEN) $(KUSTOMIZE) #EXHELP Generate WebhookConfiguration, # Generate CRDs via our own generator hack/tools/update-crds.sh # Generate the remaining operator-controller manifests - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR) + $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR)/standard + $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS),experimental rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR)/experimental # Generate the remaining catalogd manifests $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) rbac:roleName=manager-role paths="./internal/catalogd/..." output:rbac:artifacts:config=$(KUSTOMIZE_CATD_RBAC_DIR) $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) webhook paths="./internal/catalogd/..." output:webhook:artifacts:config=$(KUSTOMIZE_CATD_WEBHOOKS_DIR) @@ -164,7 +165,7 @@ manifests: $(CONTROLLER_GEN) $(KUSTOMIZE) #EXHELP Generate WebhookConfiguration, .PHONY: generate generate: $(CONTROLLER_GEN) #EXHELP Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. @find . -name "zz_generated.deepcopy.go" -delete # Need to delete the files for them to be generated properly - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) object:headerFile="hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS),experimental object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: verify verify: k8s-pin kind-verify-versions fmt generate manifests crd-ref-docs generate-test-data #HELP Verify all generated code is up-to-date. Runs k8s-pin instead of just tidy. @@ -380,7 +381,7 @@ BINARIES=operator-controller catalogd .PHONY: $(BINARIES) $(BINARIES): - go build $(GO_BUILD_FLAGS) $(GO_BUILD_EXTRA_FLAGS) -tags '$(GO_BUILD_TAGS)' -ldflags '$(GO_BUILD_LDFLAGS)' -gcflags '$(GO_BUILD_GCFLAGS)' -asmflags '$(GO_BUILD_ASMFLAGS)' -o $(BUILDBIN)/$@ ./cmd/$@ + go build $(GO_BUILD_FLAGS) $(GO_BUILD_EXTRA_FLAGS) -tags '$(GO_BUILD_TAGS),experimental' -ldflags '$(GO_BUILD_LDFLAGS)' -gcflags '$(GO_BUILD_GCFLAGS)' -asmflags '$(GO_BUILD_ASMFLAGS)' -o $(BUILDBIN)/$@ ./cmd/$@ .PHONY: build-deps build-deps: manifests generate fmt diff --git a/config/base/operator-controller/rbac/experimental/kustomization.yaml b/config/base/operator-controller/rbac/experimental/kustomization.yaml new file mode 100644 index 000000000..4d6332027 --- /dev/null +++ b/config/base/operator-controller/rbac/experimental/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- role.yaml diff --git a/config/base/operator-controller/rbac/role.yaml b/config/base/operator-controller/rbac/experimental/role.yaml similarity index 100% rename from config/base/operator-controller/rbac/role.yaml rename to config/base/operator-controller/rbac/experimental/role.yaml diff --git a/config/base/operator-controller/rbac/kustomization.yaml b/config/base/operator-controller/rbac/kustomization.yaml index 719df5654..e71d48d3c 100644 --- a/config/base/operator-controller/rbac/kustomization.yaml +++ b/config/base/operator-controller/rbac/kustomization.yaml @@ -4,8 +4,8 @@ resources: # if your manager will use a service account that exists at # runtime. Be sure to update RoleBinding and ClusterRoleBinding # subjects if changing service account names. +- standard - service_account.yaml -- role.yaml - role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml diff --git a/config/base/operator-controller/rbac/standard/kustomization.yaml b/config/base/operator-controller/rbac/standard/kustomization.yaml new file mode 100644 index 000000000..4d6332027 --- /dev/null +++ b/config/base/operator-controller/rbac/standard/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- role.yaml diff --git a/config/base/operator-controller/rbac/standard/role.yaml b/config/base/operator-controller/rbac/standard/role.yaml new file mode 100644 index 000000000..d18eb4c6c --- /dev/null +++ b/config/base/operator-controller/rbac/standard/role.yaml @@ -0,0 +1,87 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get +- apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/finalizers + verbs: + - update +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/status + verbs: + - patch + - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: manager-role + namespace: system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch diff --git a/hack/tools/update-crds.sh b/hack/tools/update-crds.sh index b86464519..3bd52b31c 100755 --- a/hack/tools/update-crds.sh +++ b/hack/tools/update-crds.sh @@ -7,11 +7,12 @@ set -e # The names of the generated CRDs CE="olm.operatorframework.io_clusterextensions.yaml" CC="olm.operatorframework.io_clustercatalogs.yaml" +CR="olm.operatorframework.io_clusterextensionrevisions.yaml" # order for modules and crds must match # each item in crds must be unique, and should be associated with a module -modules=("operator-controller" "catalogd") -crds=("${CE}" "${CC}") +modules=("operator-controller" "catalogd" "operator-controller") +crds=("${CE}" "${CC}" "${CR}") # Channels must much those in the generator channels=("standard" "experimental") @@ -38,7 +39,8 @@ done # Copy the generated files for b in ${!modules[@]}; do for c in ${channels[@]}; do - cp ${CRD_TMP}/${c}/${crds[${b}]} config/base/${modules[${b}]}/crd/${c} + SRC="${CRD_TMP}/${c}/${crds[${b}]}" + [[ -e "${SRC}" ]] && cp "${SRC}" config/base/${modules[${b}]}/crd/${c} done done From 09d98d742dddd630cd797347e726d15c79a7dfda Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 15 Jul 2025 12:20:17 +0200 Subject: [PATCH 05/10] Update boxcutter library to branch with latest k8s and controller-runtime libs Signed-off-by: Per Goncalves da Silva --- go.mod | 4 +++- go.sum | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 791ac10bf..054afcf99 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,8 @@ require ( sigs.k8s.io/yaml v1.5.0 ) +replace pkg.package-operator.run/boxcutter => github.com/perdasilva/boxcutter v0.0.0-20250715101157-18ea858f54bd + require ( k8s.io/component-helpers v0.33.2 // indirect k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect @@ -188,7 +190,7 @@ require ( github.com/proglottis/gpgme v0.1.4 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index aa9c86254..ea0757a76 100644 --- a/go.sum +++ b/go.sum @@ -398,6 +398,8 @@ github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/perdasilva/boxcutter v0.0.0-20250715101157-18ea858f54bd h1:CEXvUTPMkCMX+0q9cqwaFAl51s71qESI1V2wRK/8xsg= +github.com/perdasilva/boxcutter v0.0.0-20250715101157-18ea858f54bd/go.mod h1:74MfxxOWGkveUl9Iv2/01tg/IULlWnHEaVv2dfR4/b8= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= @@ -418,8 +420,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 h1:uTiEyEyfLhkw678n6EulHVto8AkcXVr8zUcBJNZ0ark= github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0/go.mod h1:eFYL/99JvdLP4T9/3FZ5t2pClnv7mMskc+WstTcyVr4= github.com/redis/go-redis/extra/redisotel/v9 v9.10.0 h1:4z7/hCJ9Jft8EBb2tDmK38p2WjyIEJ1ShhhwAhjOCps= @@ -781,8 +783,6 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -pkg.package-operator.run/boxcutter v0.1.2 h1:wtVxZ2cmA+fIfLUcfA8tRWhjq8MJ/52kwrYPjLwmEIo= -pkg.package-operator.run/boxcutter v0.1.2/go.mod h1:yVADyoqB8BqwubSArGBSk/WW+ZFUJgeZR+aHLHgrjbU= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFeieFlRqT627fVZ+tyfou/+S5S0H5ua0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= From dc6650128f0e1d6b9c4c74ed7257c46bdfac2384 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 15 Jul 2025 12:20:36 +0200 Subject: [PATCH 06/10] Update manifests Signed-off-by: Per Goncalves da Silva --- api/v1/zz_generated.deepcopy.go | 69 +---- .../crd/experimental/kustomization.yaml | 1 + ...ramework.io_clusterextensionrevisions.yaml | 182 +++++++++++ .../base/experimental/kustomization.yaml | 1 + .../base/standard/kustomization.yaml | 1 + .../clusterextensionrevision_controller.go | 2 + manifests/experimental-e2e.yaml | 288 ++++++++++++++++++ manifests/experimental.yaml | 288 ++++++++++++++++++ manifests/standard-e2e.yaml | 91 ++++++ manifests/standard.yaml | 91 ++++++ 10 files changed, 950 insertions(+), 64 deletions(-) create mode 100644 config/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 7e657025e..94d6e1dc7 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -405,6 +405,11 @@ func (in *ClusterExtensionRevisionPhase) DeepCopyInto(out *ClusterExtensionRevis (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Slices != nil { + in, out := &in.Slices, &out.Slices + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionPhase. @@ -530,70 +535,6 @@ func (in *ClusterExtensionStatus) DeepCopy() *ClusterExtensionStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterObjectSlice) DeepCopyInto(out *ClusterObjectSlice) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - if in.Objects != nil { - in, out := &in.Objects, &out.Objects - *out = make([]ClusterExtensionRevisionObject, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectSlice. -func (in *ClusterObjectSlice) DeepCopy() *ClusterObjectSlice { - if in == nil { - return nil - } - out := new(ClusterObjectSlice) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ClusterObjectSlice) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterObjectSliceList) DeepCopyInto(out *ClusterObjectSliceList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ClusterObjectSlice, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectSliceList. -func (in *ClusterObjectSliceList) DeepCopy() *ClusterObjectSliceList { - if in == nil { - return nil - } - out := new(ClusterObjectSliceList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ClusterObjectSliceList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImageSource) DeepCopyInto(out *ImageSource) { *out = *in diff --git a/config/base/operator-controller/crd/experimental/kustomization.yaml b/config/base/operator-controller/crd/experimental/kustomization.yaml index 1c4db41af..f0315ce34 100644 --- a/config/base/operator-controller/crd/experimental/kustomization.yaml +++ b/config/base/operator-controller/crd/experimental/kustomization.yaml @@ -1,2 +1,3 @@ resources: - olm.operatorframework.io_clusterextensions.yaml +- olm.operatorframework.io_clusterextensionrevisions.yaml diff --git a/config/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml b/config/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml new file mode 100644 index 000000000..4eba98633 --- /dev/null +++ b/config/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml @@ -0,0 +1,182 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + olm.operatorframework.io/generator: experimental + name: clusterextensionrevisions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtensionRevision + listKind: ClusterExtensionRevisionList + plural: clusterextensionrevisions + singular: clusterextensionrevision + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + lifecycleState: + default: Active + description: Specifies the lifecycle state of the ClusterExtensionRevision. + enum: + - Active + - Paused + - Archived + type: string + x-kubernetes-validations: + - message: can not un-archive + rule: oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' + && oldSelf == self + phases: + items: + properties: + name: + type: string + objects: + items: + properties: + object: + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + required: + - object + type: object + type: array + slices: + items: + type: string + type: array + required: + - name + - objects + type: object + type: array + x-kubernetes-validations: + - message: phases is immutable + rule: self == oldSelf || oldSelf.size() == 0 + previous: + items: + properties: + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - uid + type: object + type: array + x-kubernetes-validations: + - message: previous is immutable + rule: self == oldSelf + revision: + format: int64 + type: integer + x-kubernetes-validations: + - message: revision is immutable + rule: self == oldSelf + required: + - phases + - revision + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/components/base/experimental/kustomization.yaml b/config/components/base/experimental/kustomization.yaml index b9ccb1d42..62dad1a82 100644 --- a/config/components/base/experimental/kustomization.yaml +++ b/config/components/base/experimental/kustomization.yaml @@ -4,6 +4,7 @@ kind: Component resources: - ../../../base/catalogd/crd/experimental - ../../../base/operator-controller/crd/experimental +- ../../../base/operator-controller/rbac/experimental # Pull in the component(s) common to standard and experimental components: - ../common diff --git a/config/components/base/standard/kustomization.yaml b/config/components/base/standard/kustomization.yaml index bf2466405..362ab67d3 100644 --- a/config/components/base/standard/kustomization.yaml +++ b/config/components/base/standard/kustomization.yaml @@ -4,6 +4,7 @@ kind: Component resources: - ../../../base/catalogd/crd/standard - ../../../base/operator-controller/crd/standard +- ../../../base/operator-controller/rbac/standard # Pull in the component(s) common to standard and experimental components: - ../common diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go index 29d18f17d..cbb7a1b5b 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -1,3 +1,5 @@ +//go:build experimental + package controllers import ( diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index d3adf46e5..f90f4e179 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -454,6 +454,189 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + olm.operatorframework.io/feature-set: experimental + olm.operatorframework.io/generator: experimental + name: clusterextensionrevisions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtensionRevision + listKind: ClusterExtensionRevisionList + plural: clusterextensionrevisions + singular: clusterextensionrevision + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + lifecycleState: + default: Active + description: Specifies the lifecycle state of the ClusterExtensionRevision. + enum: + - Active + - Paused + - Archived + type: string + x-kubernetes-validations: + - message: can not un-archive + rule: oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' + && oldSelf == self + phases: + items: + properties: + name: + type: string + objects: + items: + properties: + object: + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + required: + - object + type: object + type: array + slices: + items: + type: string + type: array + required: + - name + - objects + type: object + type: array + x-kubernetes-validations: + - message: phases is immutable + rule: self == oldSelf || oldSelf.size() == 0 + previous: + items: + properties: + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - uid + type: object + type: array + x-kubernetes-validations: + - message: previous is immutable + rule: self == oldSelf + revision: + format: int64 + type: integer + x-kubernetes-validations: + - message: revision is immutable + rule: self == oldSelf + required: + - phases + - revision + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 @@ -1125,6 +1308,36 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + name: manager-role + namespace: olmv1-system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: annotations: olm.operatorframework.io/feature-set: experimental @@ -1267,6 +1480,81 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + name: manager-role +rules: +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get +- apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensionrevisions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensionrevisions/finalizers + - clusterextensions/finalizers + verbs: + - update +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensionrevisions/status + - clusterextensions/status + verbs: + - patch + - update +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: annotations: olm.operatorframework.io/feature-set: experimental diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index 7b0d2b9a3..ff8265f61 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -454,6 +454,189 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + olm.operatorframework.io/feature-set: experimental + olm.operatorframework.io/generator: experimental + name: clusterextensionrevisions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtensionRevision + listKind: ClusterExtensionRevisionList + plural: clusterextensionrevisions + singular: clusterextensionrevision + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + lifecycleState: + default: Active + description: Specifies the lifecycle state of the ClusterExtensionRevision. + enum: + - Active + - Paused + - Archived + type: string + x-kubernetes-validations: + - message: can not un-archive + rule: oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' + && oldSelf == self + phases: + items: + properties: + name: + type: string + objects: + items: + properties: + object: + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + required: + - object + type: object + type: array + slices: + items: + type: string + type: array + required: + - name + - objects + type: object + type: array + x-kubernetes-validations: + - message: phases is immutable + rule: self == oldSelf || oldSelf.size() == 0 + previous: + items: + properties: + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - uid + type: object + type: array + x-kubernetes-validations: + - message: previous is immutable + rule: self == oldSelf + revision: + format: int64 + type: integer + x-kubernetes-validations: + - message: revision is immutable + rule: self == oldSelf + required: + - phases + - revision + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 @@ -1125,6 +1308,36 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + name: manager-role + namespace: olmv1-system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: annotations: olm.operatorframework.io/feature-set: experimental @@ -1267,6 +1480,81 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + name: manager-role +rules: +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get +- apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensionrevisions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensionrevisions/finalizers + - clusterextensions/finalizers + verbs: + - update +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensionrevisions/status + - clusterextensions/status + verbs: + - patch + - update +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: annotations: olm.operatorframework.io/feature-set: experimental diff --git a/manifests/standard-e2e.yaml b/manifests/standard-e2e.yaml index a8aff9838..60cf7df9f 100644 --- a/manifests/standard-e2e.yaml +++ b/manifests/standard-e2e.yaml @@ -1125,6 +1125,36 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + name: manager-role + namespace: olmv1-system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: annotations: olm.operatorframework.io/feature-set: standard-e2e @@ -1267,6 +1297,67 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + name: manager-role +rules: +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get +- apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/finalizers + verbs: + - update +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/status + verbs: + - patch + - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: annotations: olm.operatorframework.io/feature-set: standard-e2e diff --git a/manifests/standard.yaml b/manifests/standard.yaml index fa2546305..08bc09646 100644 --- a/manifests/standard.yaml +++ b/manifests/standard.yaml @@ -1125,6 +1125,36 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + name: manager-role + namespace: olmv1-system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: annotations: olm.operatorframework.io/feature-set: standard @@ -1267,6 +1297,67 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + name: manager-role +rules: +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get +- apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/finalizers + verbs: + - update +- apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/status + verbs: + - patch + - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: annotations: olm.operatorframework.io/feature-set: standard From cb0e1b2d0f30713fa44616aec666ed379df7ac16 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 15 Jul 2025 13:44:42 +0200 Subject: [PATCH 07/10] Fix up makefile Signed-off-by: Per Goncalves da Silva --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 535f69bb8..9462414ba 100644 --- a/Makefile +++ b/Makefile @@ -150,8 +150,8 @@ manifests: $(CONTROLLER_GEN) $(KUSTOMIZE) #EXHELP Generate WebhookConfiguration, # Generate CRDs via our own generator hack/tools/update-crds.sh # Generate the remaining operator-controller manifests - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR)/standard - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS),experimental rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR)/experimental + $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS),standard rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR)/standard + $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR)/experimental # Generate the remaining catalogd manifests $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) rbac:roleName=manager-role paths="./internal/catalogd/..." output:rbac:artifacts:config=$(KUSTOMIZE_CATD_RBAC_DIR) $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) webhook paths="./internal/catalogd/..." output:webhook:artifacts:config=$(KUSTOMIZE_CATD_WEBHOOKS_DIR) @@ -165,7 +165,7 @@ manifests: $(CONTROLLER_GEN) $(KUSTOMIZE) #EXHELP Generate WebhookConfiguration, .PHONY: generate generate: $(CONTROLLER_GEN) #EXHELP Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. @find . -name "zz_generated.deepcopy.go" -delete # Need to delete the files for them to be generated properly - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS),experimental object:headerFile="hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: verify verify: k8s-pin kind-verify-versions fmt generate manifests crd-ref-docs generate-test-data #HELP Verify all generated code is up-to-date. Runs k8s-pin instead of just tidy. From fa7e3e229e11bd9f86fb42cdf8468425a341e59f Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 15 Jul 2025 13:55:24 +0200 Subject: [PATCH 08/10] Fix lint Signed-off-by: Per Goncalves da Silva --- cmd/operator-controller/main.go | 19 +++-- .../operator-controller/applier/boxcutter.go | 19 ++--- .../clusterextensionrevision_controller.go | 76 +++++++------------ 3 files changed, 46 insertions(+), 68 deletions(-) diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index c3da78e2f..71cac5b52 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -22,13 +22,6 @@ import ( "errors" "flag" "fmt" - "github.com/operator-framework/operator-controller/internal/operator-controller/authorization" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/certproviders" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" - apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" "net/http" "os" "path/filepath" @@ -38,7 +31,7 @@ import ( "github.com/containers/image/v5/types" "github.com/spf13/cobra" rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/labels" + apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" k8slabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" k8stypes "k8s.io/apimachinery/pkg/types" @@ -66,6 +59,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/action" "github.com/operator-framework/operator-controller/internal/operator-controller/applier" "github.com/operator-framework/operator-controller/internal/operator-controller/authentication" + "github.com/operator-framework/operator-controller/internal/operator-controller/authorization" "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/cache" catalogclient "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/client" "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager" @@ -73,6 +67,11 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/features" "github.com/operator-framework/operator-controller/internal/operator-controller/finalizers" "github.com/operator-framework/operator-controller/internal/operator-controller/resolve" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/certproviders" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" @@ -491,12 +490,12 @@ func run() error { }) // Cache scoping - req1, err := labels.NewRequirement( + req1, err := k8slabels.NewRequirement( controllers.ClusterExtensionRevisionOwnerLabel, selection.Equals, []string{ce.Name}) if err != nil { return nil, o, err } - o.DefaultLabelSelector = labels.NewSelector().Add(*req1) + o.DefaultLabelSelector = k8slabels.NewSelector().Add(*req1) return saConfig, o, nil } diff --git a/internal/operator-controller/applier/boxcutter.go b/internal/operator-controller/applier/boxcutter.go index f2b1715bf..1770eff68 100644 --- a/internal/operator-controller/applier/boxcutter.go +++ b/internal/operator-controller/applier/boxcutter.go @@ -7,20 +7,22 @@ import ( "fmt" "hash" "io/fs" + "maps" "slices" "sort" "github.com/davecgh/go-spew/spew" - ocv1 "github.com/operator-framework/operator-controller/api/v1" - "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" ) const ( @@ -66,13 +68,8 @@ func (bc *Boxcutter) apply( // objectLabels objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain)) for _, obj := range plain { - labels := obj.GetLabels() - if labels == nil { - labels = map[string]string{} - } - for k, v := range objectLabels { - labels[k] = v - } + labels := maps.Clone(obj.GetLabels()) + maps.Copy(labels, objectLabels) obj.SetLabels(labels) gvk, err := apiutil.GVKForObject(obj, bc.Scheme) diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go index cbb7a1b5b..66d76917b 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -1,4 +1,4 @@ -//go:build experimental +//go:build !standard package controllers @@ -9,7 +9,6 @@ import ( "strings" "time" - ocv1 "github.com/operator-framework/operator-controller/api/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -33,6 +32,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/source" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" ) const ( @@ -65,17 +66,17 @@ type accessManager interface { //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions/status,verbs=update;patch //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions/finalizers,verbs=update -func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { +func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx).WithName("cluster-extension-revision") ctx = log.IntoContext(ctx, l) rev := &ocv1.ClusterExtensionRevision{} if err := c.Client.Get( ctx, req.NamespacedName, rev); err != nil { - return res, client.IgnoreNotFound(err) + return ctrl.Result{}, client.IgnoreNotFound(err) } - l = l.WithValues("key", req.NamespacedName.String()) + l = l.WithValues("key", req.String()) l.Info("reconcile starting") defer l.Info("reconcile ending") @@ -86,22 +87,14 @@ func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req // To not leave unactionable resources on the cluster, we are going to just // reap the revision reverences and propagate the Orphan deletion. if rev.DeletionTimestamp.IsZero() { - err := client.IgnoreNotFound( + return ctrl.Result{}, client.IgnoreNotFound( c.Client.Delete(ctx, rev, client.PropagationPolicy(metav1.DeletePropagationOrphan), client.Preconditions{ UID: ptr.To(rev.GetUID()), ResourceVersion: ptr.To(rev.GetResourceVersion()), }), ) - if err != nil { - return res, err - } - // we get requeued to remove the finalizer. - return res, nil - } - if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { - return res, err } - return res, nil + return ctrl.Result{}, c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer) } return c.reconcile(ctx, controller, rev) @@ -109,13 +102,10 @@ func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req func (c *ClusterExtensionRevisionReconciler) reconcile( ctx context.Context, ce *ocv1.ClusterExtension, rev *ocv1.ClusterExtensionRevision, -) (res ctrl.Result, err error) { +) (ctrl.Result, error) { l := log.FromContext(ctx) - revision, opts, previous, err := toBoxcutterRevision(ce.Name, rev) - if err != nil { - return res, fmt.Errorf("converting CM to revision: %w", err) - } + revision, opts, previous := toBoxcutterRevision(ce.Name, rev) var objects []client.Object for _, phase := range revision.GetPhases() { @@ -131,13 +121,13 @@ func (c *ClusterExtensionRevisionReconciler) reconcile( // We can't lookup the complete ClusterExtension when it's already deleted. // This only works when the controller-manager is not restarted during teardown. if err := c.Client.Get(ctx, client.ObjectKeyFromObject(ce), ce); err != nil { - return res, err + return ctrl.Result{}, err } } accessor, err := c.AccessManager.GetWithUser(ctx, ce, rev, objects) if err != nil { - return res, fmt.Errorf("get cache: %w", err) + return ctrl.Result{}, fmt.Errorf("get cache: %w", err) } // Boxcutter machinery setup. @@ -160,32 +150,29 @@ func (c *ClusterExtensionRevisionReconciler) reconcile( // tres, err := re.Teardown(ctx, *revision) if err != nil { - return res, fmt.Errorf("revision teardown: %w", err) + return ctrl.Result{}, fmt.Errorf("revision teardown: %w", err) } l.Info("teardown report", "report", tres.String()) if !tres.IsComplete() { - return res, nil + return ctrl.Result{}, nil } if err := c.AccessManager.FreeWithUser(ctx, ce, rev); err != nil { - return res, fmt.Errorf("get cache: %w", err) - } - if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { - return res, err + return ctrl.Result{}, fmt.Errorf("get cache: %w", err) } - return res, nil + return ctrl.Result{}, c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer) } // // Reconcile // if err := c.ensureFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { - return res, err + return ctrl.Result{}, err } rres, err := re.Reconcile(ctx, *revision, opts...) if err != nil { - return res, fmt.Errorf("revision reconcile: %w", err) + return ctrl.Result{}, fmt.Errorf("revision reconcile: %w", err) } l.Info("reconcile report", "report", rres.String()) @@ -193,27 +180,22 @@ func (c *ClusterExtensionRevisionReconciler) reconcile( // TODO: report status, backoff? if verr := rres.GetValidationError(); verr != nil { l.Info("preflight error, retrying after 10s", "err", verr.String()) - - res.RequeueAfter = 10 * time.Second - //nolint:nilerr - return res, nil + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil } for _, pres := range rres.GetPhases() { if verr := pres.GetValidationError(); verr != nil { l.Info("preflight error, retrying after 10s", "err", verr.String()) - - res.RequeueAfter = 10 * time.Second - //nolint:nilerr - return res, nil + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil } } + //nolint:nestif if rres.IsComplete() { // Archive other revisions. for _, a := range previous { if err := c.Client.Patch(ctx, a, client.RawPatch( types.MergePatchType, []byte(`{"data":{"state":"Archived"}}`))); err != nil { - return res, fmt.Errorf("archive previous Revision: %w", err) + return ctrl.Result{}, fmt.Errorf("archive previous Revision: %w", err) } } @@ -286,7 +268,7 @@ func (c *ClusterExtensionRevisionReconciler) reconcile( meta.RemoveStatusCondition(&rev.Status.Conditions, "InTransition") } - return res, c.Client.Status().Update(ctx, rev) + return ctrl.Result{}, c.Client.Status().Update(ctx, rev) } func (c *ClusterExtensionRevisionReconciler) SetupWithManager(mgr ctrl.Manager) error { @@ -373,9 +355,9 @@ func getControllingClusterExtension(obj client.Object) (*ocv1.ClusterExtension, } func toBoxcutterRevision(clusterExtensionName string, rev *ocv1.ClusterExtensionRevision) ( - r *boxcutter.Revision, opts []boxcutter.RevisionReconcileOption, previous []client.Object, err error, + *boxcutter.Revision, []boxcutter.RevisionReconcileOption, []client.Object, ) { - r = &boxcutter.Revision{ + r := &boxcutter.Revision{ Name: rev.Name, Owner: rev, Revision: rev.Spec.Revision, @@ -397,6 +379,7 @@ func toBoxcutterRevision(clusterExtensionName string, rev *ocv1.ClusterExtension r.Phases = append(r.Phases, phase) } + previous := make([]client.Object, 0, len(rev.Spec.Previous)) for _, specPrevious := range rev.Spec.Previous { prev := &unstructured.Unstructured{} prev.SetName(specPrevious.Name) @@ -405,9 +388,9 @@ func toBoxcutterRevision(clusterExtensionName string, rev *ocv1.ClusterExtension previous = append(previous, prev) } - opts = []boxcutter.RevisionReconcileOption{ + opts := []boxcutter.RevisionReconcileOption{ boxcutter.WithPreviousOwners(previous), - boxcutter.WithProbe(boxcutter.ProgressProbeType, boxcutter.ProbeFunc(func(obj client.Object) (success bool, messages []string) { + boxcutter.WithProbe(boxcutter.ProgressProbeType, boxcutter.ProbeFunc(func(obj client.Object) (bool, []string) { deployGK := schema.GroupKind{ Group: "apps", Kind: "Deployment", } @@ -430,12 +413,11 @@ func toBoxcutterRevision(clusterExtensionName string, rev *ocv1.ClusterExtension return true, nil } } - return false, []string{"not available or not fully updated"} })), } if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStatePaused { opts = append(opts, boxcutter.WithPaused{}) } - return + return r, opts, previous } From 47da1c13c315955679d280f38bf7c043fb7f732d Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 15 Jul 2025 13:58:29 +0200 Subject: [PATCH 09/10] Remove ClusterExtensionRevision from crd-docs Signed-off-by: Per Goncalves da Silva --- docs/api-reference/crd-ref-docs-gen-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/crd-ref-docs-gen-config.yaml b/docs/api-reference/crd-ref-docs-gen-config.yaml index f6394fdf6..c8efa15c1 100644 --- a/docs/api-reference/crd-ref-docs-gen-config.yaml +++ b/docs/api-reference/crd-ref-docs-gen-config.yaml @@ -1,5 +1,5 @@ processor: - ignoreTypes: [] + ignoreTypes: [ClusterExtensionRevision, ClusterExtensionRevisionList] ignoreFields: [] render: From 39ee577b9d5d42141c01ff260be0c1cd61b99679 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 15 Jul 2025 14:25:49 +0200 Subject: [PATCH 10/10] Fixes Signed-off-by: Per Goncalves da Silva --- cmd/operator-controller/main.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 71cac5b52..5d317bbb1 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -523,15 +523,17 @@ func run() error { return err } - if err = (&controllers.ClusterExtensionRevisionReconciler{ - Client: cl, - AccessManager: accessManager, - Scheme: mgr.GetScheme(), - RestMapper: mgr.GetRESTMapper(), - DiscoveryClient: discoveryClient, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") - return err + if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) { + if err = (&controllers.ClusterExtensionRevisionReconciler{ + Client: cl, + AccessManager: accessManager, + Scheme: mgr.GetScheme(), + RestMapper: mgr.GetRESTMapper(), + DiscoveryClient: discoveryClient, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") + return err + } } if err = (&controllers.ClusterCatalogReconciler{