From 17df817707457511745d64c5f253603c46f4b731 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 8 Jul 2025 15:46:54 +0200 Subject: [PATCH 1/4] Update test-operator v1.0.0 Signed-off-by: Per Goncalves da Silva --- .../v1.0.0/api/v1/groupversion_info.go | 36 +++ .../v1.0.0/api/v1/testoperator_types.go | 57 ++++ .../v1.0.0/api/v1/zz_generated.deepcopy.go | 114 ++++++++ .../bundles/test-operator/v1.0.0/cmd/main.go | 255 ++++++++++++++++++ .../controller/testoperator_controller.go | 110 ++++++++ .../testoperator_controller_test.go | 137 ++++++++++ .../olm.operatorframework.com_olme2etest.yaml | 28 -- ...operator.v1.0.0.clusterserviceversion.yaml | 190 +++++++++++++ .../testoperator.clusterserviceversion.yaml | 141 ---------- ...tors.testolm.operatorframework.io.crd.yaml | 61 +++++ .../v1.0.0/metadata/annotations.yaml | 4 - 11 files changed, 960 insertions(+), 173 deletions(-) create mode 100644 testdata/images/bundles/test-operator/v1.0.0/api/v1/groupversion_info.go create mode 100644 testdata/images/bundles/test-operator/v1.0.0/api/v1/testoperator_types.go create mode 100644 testdata/images/bundles/test-operator/v1.0.0/api/v1/zz_generated.deepcopy.go create mode 100644 testdata/images/bundles/test-operator/v1.0.0/cmd/main.go create mode 100644 testdata/images/bundles/test-operator/v1.0.0/internal/controller/testoperator_controller.go create mode 100644 testdata/images/bundles/test-operator/v1.0.0/internal/controller/testoperator_controller_test.go delete mode 100644 testdata/images/bundles/test-operator/v1.0.0/manifests/olm.operatorframework.com_olme2etest.yaml create mode 100644 testdata/images/bundles/test-operator/v1.0.0/manifests/test-operator.v1.0.0.clusterserviceversion.yaml delete mode 100644 testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.clusterserviceversion.yaml create mode 100644 testdata/images/bundles/test-operator/v1.0.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml diff --git a/testdata/images/bundles/test-operator/v1.0.0/api/v1/groupversion_info.go b/testdata/images/bundles/test-operator/v1.0.0/api/v1/groupversion_info.go new file mode 100644 index 000000000..e182c6436 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2025. + +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 contains API Schema definitions for the testolm v1 API group. +// +kubebuilder:object:generate=false +// +groupName=testolm.operatorframework.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "testolm.operatorframework.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/testdata/images/bundles/test-operator/v1.0.0/api/v1/testoperator_types.go b/testdata/images/bundles/test-operator/v1.0.0/api/v1/testoperator_types.go new file mode 100644 index 000000000..0da2557f0 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/api/v1/testoperator_types.go @@ -0,0 +1,57 @@ +/* +Copyright 2025. + +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" +) + +// TestOperatorSpec defines the desired state of TestOperator. +type TestOperatorSpec struct { + // +optional + Message string `json:"message,omitempty"` +} + +// TestOperatorStatus defines the observed state of TestOperator. +type TestOperatorStatus struct { + Echo string `json:"echo,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// TestOperator is the Schema for the testoperators API. +type TestOperator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TestOperatorSpec `json:"spec,omitempty"` + Status TestOperatorStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// TestOperatorList contains a list of TestOperator. +type TestOperatorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestOperator `json:"items"` +} + +func init() { + SchemeBuilder.Register(&TestOperator{}, &TestOperatorList{}) +} diff --git a/testdata/images/bundles/test-operator/v1.0.0/api/v1/zz_generated.deepcopy.go b/testdata/images/bundles/test-operator/v1.0.0/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..9b28ef91c --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,114 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestOperator) DeepCopyInto(out *TestOperator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperator. +func (in *TestOperator) DeepCopy() *TestOperator { + if in == nil { + return nil + } + out := new(TestOperator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestOperator) 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 *TestOperatorList) DeepCopyInto(out *TestOperatorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestOperator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorList. +func (in *TestOperatorList) DeepCopy() *TestOperatorList { + if in == nil { + return nil + } + out := new(TestOperatorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestOperatorList) 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 *TestOperatorSpec) DeepCopyInto(out *TestOperatorSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorSpec. +func (in *TestOperatorSpec) DeepCopy() *TestOperatorSpec { + if in == nil { + return nil + } + out := new(TestOperatorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestOperatorStatus) DeepCopyInto(out *TestOperatorStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorStatus. +func (in *TestOperatorStatus) DeepCopy() *TestOperatorStatus { + if in == nil { + return nil + } + out := new(TestOperatorStatus) + in.DeepCopyInto(out) + return out +} diff --git a/testdata/images/bundles/test-operator/v1.0.0/cmd/main.go b/testdata/images/bundles/test-operator/v1.0.0/cmd/main.go new file mode 100644 index 000000000..8b494de95 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/cmd/main.go @@ -0,0 +1,255 @@ +/* +Copyright 2025. + +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 main + +import ( + "crypto/tls" + "flag" + "k8s.io/klog/v2" + "os" + "path/filepath" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + testolmv1 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v1.0.0/api/v1" + "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v1.0.0/internal/controller" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(testolmv1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +// nolint:gocyclo +func main() { + var metricsAddr string + var metricsCertPath, metricsCertName, metricsCertKey string + var webhookCertPath, webhookCertName, webhookCertKey string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", true, + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") + flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") + flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") + flag.StringVar(&metricsCertPath, "metrics-cert-path", "", + "The directory that contains the metrics server certificate.") + flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") + flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + + //add klog flags to flagset + klog.InitFlags(flag.CommandLine) + ctrl.SetLogger(klog.NewKlogr()) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + // Create watchers for metrics and webhooks certificates + var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher + + // Initial webhook TLS options + webhookTLSOpts := tlsOpts + + if len(webhookCertPath) > 0 { + setupLog.Info("Initializing webhook certificate watcher using provided certificates", + "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) + + var err error + webhookCertWatcher, err = certwatcher.New( + filepath.Join(webhookCertPath, webhookCertName), + filepath.Join(webhookCertPath, webhookCertKey), + ) + if err != nil { + setupLog.Error(err, "Failed to initialize webhook certificate watcher") + os.Exit(1) + } + + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { + config.GetCertificate = webhookCertWatcher.GetCertificate + }) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: webhookTLSOpts, + }) + + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + } + + if secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + // If the certificate is not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + // + // TODO(user): If you enable certManager, uncomment the following lines: + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates + // managed by cert-manager for the metrics server. + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + if len(metricsCertPath) > 0 { + setupLog.Info("Initializing metrics certificate watcher using provided certificates", + "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) + + var err error + metricsCertWatcher, err = certwatcher.New( + filepath.Join(metricsCertPath, metricsCertName), + filepath.Join(metricsCertPath, metricsCertKey), + ) + if err != nil { + setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) + os.Exit(1) + } + + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { + config.GetCertificate = metricsCertWatcher.GetCertificate + }) + } + + watchNamespace := getWatchNamespace() + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "e10ca34c.operatorframework.io", + Cache: cache.Options{ + DefaultNamespaces: map[string]cache.Config{watchNamespace: {}}, + }, + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err := (&controller.TestOperatorReconciler{ + Client: mgr.GetClient(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "TestOperator") + os.Exit(1) + } + // +kubebuilder:scaffold:builder + + if metricsCertWatcher != nil { + setupLog.Info("Adding metrics certificate watcher to manager") + if err := mgr.Add(metricsCertWatcher); err != nil { + setupLog.Error(err, "unable to add metrics certificate watcher to manager") + os.Exit(1) + } + } + + if webhookCertWatcher != nil { + setupLog.Info("Adding webhook certificate watcher to manager") + if err := mgr.Add(webhookCertWatcher); err != nil { + setupLog.Error(err, "unable to add webhook certificate watcher to manager") + os.Exit(1) + } + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +// getWatchNamespace returns the Namespace the operator should be watching for changes +func getWatchNamespace() string { + // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE + // which specifies the Namespace to watch. + // An empty value means the operator is running with cluster scope. + var watchNamespaceEnvVar = "WATCH_NAMESPACE" + + ns, _ := os.LookupEnv(watchNamespaceEnvVar) + return ns +} diff --git a/testdata/images/bundles/test-operator/v1.0.0/internal/controller/testoperator_controller.go b/testdata/images/bundles/test-operator/v1.0.0/internal/controller/testoperator_controller.go new file mode 100644 index 000000000..8813379e9 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/internal/controller/testoperator_controller.go @@ -0,0 +1,110 @@ +/* +Copyright 2025. + +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 controller + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/equality" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" + "sigs.k8s.io/controller-runtime/pkg/log" + + testolmv1 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v1.0.0/api/v1" +) + +const ( + peaceOutFinalizer = "olm.operatorframework.io/peace-out" +) + +// TestOperatorReconciler reconciles a TestOperator object +type TestOperatorReconciler struct { + client.Client + Finalizers crfinalizer.Finalizers +} + +// +kubebuilder:rbac:groups=testolm.operatorframework.io,resources=testoperators,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=testolm.operatorframework.io,resources=testoperators/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=testolm.operatorframework.io,resources=testoperators/finalizers,verbs=update + +func (r *TestOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx).WithName("test-operator") + ctx = log.IntoContext(ctx, l) + + l.Info("reconcile starting") + defer l.Info("reconcile ending") + + existingTestOp := &testolmv1.TestOperator{} + if err := r.Get(ctx, req.NamespacedName, existingTestOp); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Reconcile TestOperator + reconciledTestOp := existingTestOp.DeepCopy() + + // Reconcile finalizer + finalizeResult, err := r.Finalizers.Finalize(ctx, reconciledTestOp) + if err != nil { + return ctrl.Result{}, err + } + if finalizeResult.Updated || finalizeResult.StatusUpdated { + return ctrl.Result{}, r.Update(ctx, reconciledTestOp) + } + + // Reconcile status + reconciledTestOp.Status.Echo = reconciledTestOp.Spec.Message + + if !equality.Semantic.DeepEqual(existingTestOp.Status, reconciledTestOp.Status) { + return ctrl.Result{}, r.Client.Status().Update(ctx, reconciledTestOp) + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *TestOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := r.setupFinalizers(); err != nil { + return fmt.Errorf("failed to setup finalizers: %v", err) + } + return ctrl.NewControllerManagedBy(mgr). + For(&testolmv1.TestOperator{}). + Named("testoperator"). + Complete(r) +} + +type finalizerFunc func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) + +func (f finalizerFunc) Finalize(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { + return f(ctx, obj) +} + +func (r *TestOperatorReconciler) setupFinalizers() error { + f := crfinalizer.NewFinalizers() + err := f.Register(peaceOutFinalizer, finalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { + if _, ok := obj.(*testolmv1.TestOperator); !ok { + panic("could not convert object to testoperator") + } + log.FromContext(ctx).Info("peace out, bruh!") + return crfinalizer.Result{StatusUpdated: true}, nil + })) + if err != nil { + return err + } + r.Finalizers = f + return nil +} diff --git a/testdata/images/bundles/test-operator/v1.0.0/internal/controller/testoperator_controller_test.go b/testdata/images/bundles/test-operator/v1.0.0/internal/controller/testoperator_controller_test.go new file mode 100644 index 000000000..10be7cb36 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/internal/controller/testoperator_controller_test.go @@ -0,0 +1,137 @@ +/* +Copyright 2025. + +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 controller_test + +import ( + "context" + "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v1.0.0/internal/controller" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" + "log" + "os" + "path/filepath" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "testing" + + testolmv1 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v1.0.0/api/v1" +) + +func Test_Reconcile(t *testing.T) { + cl, reconciler := newClientAndReconciler(t) + + const resourceName = "test-resource" + + ctx := context.Background() + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + testoperator := &testolmv1.TestOperator{} + + err := cl.Get(ctx, typeNamespacedName, testoperator) + if err != nil && errors.IsNotFound(err) { + resource := &testolmv1.TestOperator{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: testolmv1.TestOperatorSpec{ + Message: "this is the message", + }, + } + require.NoError(t, cl.Create(ctx, resource)) + defer func() { + t.Log("Cleanup the specific resource instance TestOperator") + require.NoError(t, cl.Delete(ctx, resource)) + }() + } + + require.NoError(t, cl.Get(ctx, typeNamespacedName, testoperator)) + + t.Log("Reconciling the created resource") + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + require.NoError(t, err) + + t.Log("Checking that the resource was reconciled successfully") + err = cl.Get(ctx, typeNamespacedName, testoperator) + require.NoError(t, err) + require.Equal(t, testoperator.Status.Echo, "this is the message") +} + +var ( + config *rest.Config +) + +func TestMain(m *testing.M) { + testEnv := &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "manifests"), + }, + ErrorIfCRDPathMissing: true, + } + + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + + var err error + config, err = testEnv.Start() + utilruntime.Must(err) + if config == nil { + log.Panic("expected cfg to not be nil") + } + + code := m.Run() + utilruntime.Must(testEnv.Stop()) + os.Exit(code) +} + +// getFirstFoundEnvTestBinaryDir finds and returns the first directory under the given path. +func getFirstFoundEnvTestBinaryDir() string { + basePath := filepath.Join("..", "..", "..", "..", "..", "..", "..", "bin", "envtest-binaries", "k8s") + entries, _ := os.ReadDir(basePath) + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} + +func newClientAndReconciler(t *testing.T) (client.Client, *controller.TestOperatorReconciler) { + sch := apimachineryruntime.NewScheme() + require.NoError(t, testolmv1.AddToScheme(sch)) + cl, err := client.New(config, client.Options{Scheme: sch}) + require.NoError(t, err) + require.NotNil(t, cl) + + reconciler := &controller.TestOperatorReconciler{ + Client: cl, + Finalizers: crfinalizer.NewFinalizers(), + } + return cl, reconciler +} diff --git a/testdata/images/bundles/test-operator/v1.0.0/manifests/olm.operatorframework.com_olme2etest.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/olm.operatorframework.com_olme2etest.yaml deleted file mode 100644 index fcfd4aeaf..000000000 --- a/testdata/images/bundles/test-operator/v1.0.0/manifests/olm.operatorframework.com_olme2etest.yaml +++ /dev/null @@ -1,28 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.1 - name: olme2etests.olm.operatorframework.io -spec: - group: olm.operatorframework.io - names: - kind: OLME2ETest - listKind: OLME2ETestList - plural: olme2etests - singular: olme2etest - scope: Cluster - versions: - - name: v1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - testField: - type: string diff --git a/testdata/images/bundles/test-operator/v1.0.0/manifests/test-operator.v1.0.0.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/test-operator.v1.0.0.clusterserviceversion.yaml new file mode 100644 index 000000000..ff9963703 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/manifests/test-operator.v1.0.0.clusterserviceversion.yaml @@ -0,0 +1,190 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + createdAt: "2025-07-03T15:15:31Z" + operators.operatorframework.io/builder: operator-sdk-v1.40.0+git + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: test-operator.v1.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: TestOperator is the Schema for the testoperators API. + displayName: Test Operator + kind: TestOperator + name: testoperators.testolm.operatorframework.io + version: v1 + description: Test OLM Operator + displayName: OLM Test Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators/finalizers + verbs: + - update + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators/status + verbs: + - get + - patch + - update + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: test-operator-controller-manager + deployments: + - label: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: test-operator + control-plane: controller-manager + name: test-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: test-operator + control-plane: controller-manager + strategy: {} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + app.kubernetes.io/name: test-operator + control-plane: controller-manager + spec: + containers: + - args: + - --leader-elect + - --health-probe-bind-address=:8081 + command: + - /manager + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.annotations['olm.targetNamespaces'] + image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/controllers/test-operator:v1.0.0 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: test-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: test-operator-controller-manager + strategy: deployment + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - test + - operator + links: + - name: V1 + url: https://github.com/operator-framework/operator-controller + maintainers: + - email: operator-framework-olm-dev@googlegroups.com + name: community + maturity: alpha + provider: + name: operator-framework + url: https://github.com/operator-framework/operator-controller + version: 1.0.0 diff --git a/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.clusterserviceversion.yaml deleted file mode 100644 index bdefe11fe..000000000 --- a/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.clusterserviceversion.yaml +++ /dev/null @@ -1,141 +0,0 @@ -apiVersion: operators.coreos.com/v1alpha1 -kind: ClusterServiceVersion -metadata: - annotations: - alm-examples: |- - [ - { - "apiVersion": "olme2etests.olm.operatorframework.io/v1", - "kind": "OLME2ETests", - "metadata": { - "labels": { - "app.kubernetes.io/managed-by": "kustomize", - "app.kubernetes.io/name": "test" - }, - "name": "test-sample" - }, - "spec": null - } - ] - capabilities: Basic Install - createdAt: "2024-10-24T19:21:40Z" - operators.operatorframework.io/builder: operator-sdk-v1.34.1 - operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 - name: testoperator.v1.0.0 - namespace: placeholder -spec: - apiservicedefinitions: {} - customresourcedefinitions: - owned: - - description: Configures subsections of Alertmanager configuration specific to each namespace - displayName: OLME2ETest - kind: OLME2ETest - name: olme2etests.olm.operatorframework.io - version: v1 - description: OLM E2E Testing Operator - displayName: test-operator - icon: - - base64data: "" - mediatype: "" - install: - spec: - deployments: - - label: - app.kubernetes.io/component: controller - app.kubernetes.io/name: test-operator - app.kubernetes.io/version: 1.0.0 - name: test-operator - spec: - replicas: 1 - selector: - matchLabels: - app: olme2etest - template: - metadata: - labels: - app: olme2etest - spec: - terminationGracePeriodSeconds: 0 - containers: - - name: busybox - image: busybox - command: - - 'sleep' - - '1000' - securityContext: - runAsUser: 1000 - runAsNonRoot: true - serviceAccountName: simple-bundle-manager - clusterPermissions: - - rules: - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create - serviceAccountName: simple-bundle-manager - permissions: - - rules: - - apiGroups: - - "" - resources: - - configmaps - - serviceaccounts - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch - serviceAccountName: simple-bundle-manager - strategy: deployment - installModes: - - supported: false - type: OwnNamespace - - supported: false - type: SingleNamespace - - supported: false - type: MultiNamespace - - supported: true - type: AllNamespaces - keywords: - - registry - links: - - name: simple-bundle - url: https://simple-bundle.domain - maintainers: - - email: main#simple-bundle.domain - name: Simple Bundle - maturity: beta - provider: - name: Simple Bundle - url: https://simple-bundle.domain - version: 1.0.0 diff --git a/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml new file mode 100644 index 000000000..685c165ea --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml @@ -0,0 +1,61 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + creationTimestamp: null + name: testoperators.testolm.operatorframework.io +spec: + group: testolm.operatorframework.io + names: + kind: TestOperator + listKind: TestOperatorList + plural: testoperators + singular: testoperator + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: TestOperator is the Schema for the testoperators 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: TestOperatorSpec defines the desired state of TestOperator. + properties: + message: + type: string + type: object + status: + description: TestOperatorStatus defines the observed state of TestOperator. + properties: + echo: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/testdata/images/bundles/test-operator/v1.0.0/metadata/annotations.yaml b/testdata/images/bundles/test-operator/v1.0.0/metadata/annotations.yaml index 404f0f4a3..8ba0e750b 100644 --- a/testdata/images/bundles/test-operator/v1.0.0/metadata/annotations.yaml +++ b/testdata/images/bundles/test-operator/v1.0.0/metadata/annotations.yaml @@ -1,10 +1,6 @@ annotations: - # Core bundle annotations. operators.operatorframework.io.bundle.mediatype.v1: registry+v1 operators.operatorframework.io.bundle.manifests.v1: manifests/ operators.operatorframework.io.bundle.metadata.v1: metadata/ operators.operatorframework.io.bundle.package.v1: test operators.operatorframework.io.bundle.channels.v1: beta - operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.0 - operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 - operators.operatorframework.io.metrics.project_layout: unknown From f837ca4c46fc394fba29eabb075dc51e4b40b322 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 8 Jul 2025 15:47:02 +0200 Subject: [PATCH 2/4] Update test-operator v2.0.0 Signed-off-by: Per Goncalves da Silva --- .../v2.0.0/api/v1/groupversion_info.go | 36 +++ .../v2.0.0/api/v1/testoperator_conversion.go | 47 ++++ .../v2.0.0/api/v1/testoperator_types.go | 57 ++++ .../v2.0.0/api/v1/zz_generated.deepcopy.go | 114 ++++++++ .../v2.0.0/api/v2/groupversion_info.go | 36 +++ .../v2.0.0/api/v2/testoperator_conversion.go | 20 ++ .../v2.0.0/api/v2/testoperator_types.go | 59 ++++ .../v2.0.0/api/v2/zz_generated.deepcopy.go | 114 ++++++++ .../bundles/test-operator/v2.0.0/cmd/main.go | 264 ++++++++++++++++++ .../controller/testoperator_controller.go | 110 ++++++++ .../testoperator_controller_test.go | 138 +++++++++ .../internal/webhook/testoperator_webhook.go | 120 ++++++++ .../webhook/testoperator_webhook_test.go | 62 ++++ .../v2.0.0/manifests/bundle.configmap.yaml | 12 - .../olm.operatorframework.com_olme2etest.yaml | 28 -- ...operator.v2.0.0.clusterserviceversion.yaml | 252 +++++++++++++++++ .../testoperator.clusterserviceversion.yaml | 141 ---------- ...tors.testolm.operatorframework.io.crd.yaml | 110 ++++++++ .../v2.0.0/metadata/annotations.yaml | 4 - 19 files changed, 1539 insertions(+), 185 deletions(-) create mode 100644 testdata/images/bundles/test-operator/v2.0.0/api/v1/groupversion_info.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/api/v1/testoperator_conversion.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/api/v1/testoperator_types.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/api/v1/zz_generated.deepcopy.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/api/v2/groupversion_info.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/api/v2/testoperator_conversion.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/api/v2/testoperator_types.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/api/v2/zz_generated.deepcopy.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/cmd/main.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/internal/controller/testoperator_controller.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/internal/controller/testoperator_controller_test.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/internal/webhook/testoperator_webhook.go create mode 100644 testdata/images/bundles/test-operator/v2.0.0/internal/webhook/testoperator_webhook_test.go delete mode 100644 testdata/images/bundles/test-operator/v2.0.0/manifests/bundle.configmap.yaml delete mode 100644 testdata/images/bundles/test-operator/v2.0.0/manifests/olm.operatorframework.com_olme2etest.yaml create mode 100644 testdata/images/bundles/test-operator/v2.0.0/manifests/test-operator.v2.0.0.clusterserviceversion.yaml delete mode 100644 testdata/images/bundles/test-operator/v2.0.0/manifests/testoperator.clusterserviceversion.yaml create mode 100644 testdata/images/bundles/test-operator/v2.0.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml diff --git a/testdata/images/bundles/test-operator/v2.0.0/api/v1/groupversion_info.go b/testdata/images/bundles/test-operator/v2.0.0/api/v1/groupversion_info.go new file mode 100644 index 000000000..e182c6436 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2025. + +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 contains API Schema definitions for the testolm v1 API group. +// +kubebuilder:object:generate=false +// +groupName=testolm.operatorframework.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "testolm.operatorframework.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/testdata/images/bundles/test-operator/v2.0.0/api/v1/testoperator_conversion.go b/testdata/images/bundles/test-operator/v2.0.0/api/v1/testoperator_conversion.go new file mode 100644 index 000000000..85f37d2bd --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/api/v1/testoperator_conversion.go @@ -0,0 +1,47 @@ +/* +Copyright 2025. + +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 ( + "log" + + "sigs.k8s.io/controller-runtime/pkg/conversion" + + testolmv2 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/api/v2" +) + +// ConvertTo converts this TestOperator (v1) to the Hub version (v2). +func (src *TestOperator) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*testolmv2.TestOperator) + log.Printf("ConvertTo: Converting TestOperator from Spoke version v1 to Hub version v2;"+ + "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) + dst.ObjectMeta = src.ObjectMeta + dst.Spec.EchoMessage = src.Spec.Message + log.Printf("ConvertedTo: %s/%s", dst.Namespace, dst.Name) + return nil +} + +// ConvertFrom converts the Hub version (v2) to this TestOperator (v1). +func (dst *TestOperator) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*testolmv2.TestOperator) + log.Printf("ConvertFrom: Converting TestOperator from Hub version v2 to Spoke version v1;"+ + "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) + dst.ObjectMeta = src.ObjectMeta + dst.Spec.Message = src.Spec.EchoMessage + log.Printf("ConvertedTo: %s/%s", dst.Namespace, dst.Name) + return nil +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/api/v1/testoperator_types.go b/testdata/images/bundles/test-operator/v2.0.0/api/v1/testoperator_types.go new file mode 100644 index 000000000..0da2557f0 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/api/v1/testoperator_types.go @@ -0,0 +1,57 @@ +/* +Copyright 2025. + +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" +) + +// TestOperatorSpec defines the desired state of TestOperator. +type TestOperatorSpec struct { + // +optional + Message string `json:"message,omitempty"` +} + +// TestOperatorStatus defines the observed state of TestOperator. +type TestOperatorStatus struct { + Echo string `json:"echo,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// TestOperator is the Schema for the testoperators API. +type TestOperator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TestOperatorSpec `json:"spec,omitempty"` + Status TestOperatorStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// TestOperatorList contains a list of TestOperator. +type TestOperatorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestOperator `json:"items"` +} + +func init() { + SchemeBuilder.Register(&TestOperator{}, &TestOperatorList{}) +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/api/v1/zz_generated.deepcopy.go b/testdata/images/bundles/test-operator/v2.0.0/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..9b28ef91c --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,114 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestOperator) DeepCopyInto(out *TestOperator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperator. +func (in *TestOperator) DeepCopy() *TestOperator { + if in == nil { + return nil + } + out := new(TestOperator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestOperator) 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 *TestOperatorList) DeepCopyInto(out *TestOperatorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestOperator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorList. +func (in *TestOperatorList) DeepCopy() *TestOperatorList { + if in == nil { + return nil + } + out := new(TestOperatorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestOperatorList) 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 *TestOperatorSpec) DeepCopyInto(out *TestOperatorSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorSpec. +func (in *TestOperatorSpec) DeepCopy() *TestOperatorSpec { + if in == nil { + return nil + } + out := new(TestOperatorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestOperatorStatus) DeepCopyInto(out *TestOperatorStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorStatus. +func (in *TestOperatorStatus) DeepCopy() *TestOperatorStatus { + if in == nil { + return nil + } + out := new(TestOperatorStatus) + in.DeepCopyInto(out) + return out +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/api/v2/groupversion_info.go b/testdata/images/bundles/test-operator/v2.0.0/api/v2/groupversion_info.go new file mode 100644 index 000000000..67fc206c8 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/api/v2/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2025. + +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 v2 contains API Schema definitions for the testolm v2 API group. +// +kubebuilder:object:generate=false +// +groupName=testolm.operatorframework.io +package v2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "testolm.operatorframework.io", Version: "v2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/testdata/images/bundles/test-operator/v2.0.0/api/v2/testoperator_conversion.go b/testdata/images/bundles/test-operator/v2.0.0/api/v2/testoperator_conversion.go new file mode 100644 index 000000000..cf8ffc056 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/api/v2/testoperator_conversion.go @@ -0,0 +1,20 @@ +/* +Copyright 2025. + +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 v2 + +// Hub marks this type as a conversion hub. +func (*TestOperator) Hub() {} diff --git a/testdata/images/bundles/test-operator/v2.0.0/api/v2/testoperator_types.go b/testdata/images/bundles/test-operator/v2.0.0/api/v2/testoperator_types.go new file mode 100644 index 000000000..2b65c6d08 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/api/v2/testoperator_types.go @@ -0,0 +1,59 @@ +/* +Copyright 2025. + +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 v2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestOperatorSpec defines the desired state of TestOperator. +type TestOperatorSpec struct { + // +optional + EchoMessage string `json:"echoMessage,omitempty"` +} + +// TestOperatorStatus defines the observed state of TestOperator. +type TestOperatorStatus struct { + Echo string `json:"echo,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:conversion:hub +// +kubebuilder:subresource:status + +// TestOperator is the Schema for the testoperators API. +type TestOperator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TestOperatorSpec `json:"spec,omitempty"` + Status TestOperatorStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// TestOperatorList contains a list of TestOperator. +type TestOperatorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestOperator `json:"items"` +} + +func init() { + SchemeBuilder.Register(&TestOperator{}, &TestOperatorList{}) +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/api/v2/zz_generated.deepcopy.go b/testdata/images/bundles/test-operator/v2.0.0/api/v2/zz_generated.deepcopy.go new file mode 100644 index 000000000..0d3afbf3a --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/api/v2/zz_generated.deepcopy.go @@ -0,0 +1,114 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v2 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestOperator) DeepCopyInto(out *TestOperator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperator. +func (in *TestOperator) DeepCopy() *TestOperator { + if in == nil { + return nil + } + out := new(TestOperator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestOperator) 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 *TestOperatorList) DeepCopyInto(out *TestOperatorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestOperator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorList. +func (in *TestOperatorList) DeepCopy() *TestOperatorList { + if in == nil { + return nil + } + out := new(TestOperatorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestOperatorList) 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 *TestOperatorSpec) DeepCopyInto(out *TestOperatorSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorSpec. +func (in *TestOperatorSpec) DeepCopy() *TestOperatorSpec { + if in == nil { + return nil + } + out := new(TestOperatorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestOperatorStatus) DeepCopyInto(out *TestOperatorStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestOperatorStatus. +func (in *TestOperatorStatus) DeepCopy() *TestOperatorStatus { + if in == nil { + return nil + } + out := new(TestOperatorStatus) + in.DeepCopyInto(out) + return out +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/cmd/main.go b/testdata/images/bundles/test-operator/v2.0.0/cmd/main.go new file mode 100644 index 000000000..d9e076a12 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/cmd/main.go @@ -0,0 +1,264 @@ +/* +Copyright 2025. + +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 main + +import ( + "crypto/tls" + "flag" + testolmv1 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/api/v1" + "k8s.io/klog/v2" + "os" + "path/filepath" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + testolmv2 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/api/v2" + "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/internal/controller" + testolmwebhook "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/internal/webhook" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(testolmv1.AddToScheme(scheme)) + utilruntime.Must(testolmv2.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +// nolint:gocyclo +func main() { + var metricsAddr string + var metricsCertPath, metricsCertName, metricsCertKey string + var webhookCertPath, webhookCertName, webhookCertKey string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", true, + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") + flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") + flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") + flag.StringVar(&metricsCertPath, "metrics-cert-path", "", + "The directory that contains the metrics server certificate.") + flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") + flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + + klog.InitFlags(flag.CommandLine) + ctrl.SetLogger(klog.NewKlogr()) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + // Create watchers for metrics and webhooks certificates + var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher + + // Initial webhook TLS options + webhookTLSOpts := tlsOpts + + if len(webhookCertPath) > 0 { + setupLog.Info("Initializing webhook certificate watcher using provided certificates", + "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) + + var err error + webhookCertWatcher, err = certwatcher.New( + filepath.Join(webhookCertPath, webhookCertName), + filepath.Join(webhookCertPath, webhookCertKey), + ) + if err != nil { + setupLog.Error(err, "Failed to initialize webhook certificate watcher") + os.Exit(1) + } + + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { + config.GetCertificate = webhookCertWatcher.GetCertificate + }) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: webhookTLSOpts, + }) + + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + } + + if secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + // If the certificate is not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + // + // TODO(user): If you enable certManager, uncomment the following lines: + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates + // managed by cert-manager for the metrics server. + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + if len(metricsCertPath) > 0 { + setupLog.Info("Initializing metrics certificate watcher using provided certificates", + "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) + + var err error + metricsCertWatcher, err = certwatcher.New( + filepath.Join(metricsCertPath, metricsCertName), + filepath.Join(metricsCertPath, metricsCertKey), + ) + if err != nil { + setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) + os.Exit(1) + } + + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { + config.GetCertificate = metricsCertWatcher.GetCertificate + }) + } + + watchNamespace := getWatchNamespace() + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "e10ca34c.operatorframework.io", + Cache: cache.Options{ + DefaultNamespaces: map[string]cache.Config{watchNamespace: {}}, + }, + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err := (&controller.TestOperatorReconciler{ + Client: mgr.GetClient(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "TestOperator") + os.Exit(1) + } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err := testolmwebhook.SetupTestOperatorWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "TestOperator") + os.Exit(1) + } + } + // +kubebuilder:scaffold:builder + + if metricsCertWatcher != nil { + setupLog.Info("Adding metrics certificate watcher to manager") + if err := mgr.Add(metricsCertWatcher); err != nil { + setupLog.Error(err, "unable to add metrics certificate watcher to manager") + os.Exit(1) + } + } + + if webhookCertWatcher != nil { + setupLog.Info("Adding webhook certificate watcher to manager") + if err := mgr.Add(webhookCertWatcher); err != nil { + setupLog.Error(err, "unable to add webhook certificate watcher to manager") + os.Exit(1) + } + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +// getWatchNamespace returns the Namespace the operator should be watching for changes +func getWatchNamespace() string { + // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE + // which specifies the Namespace to watch. + // An empty value means the operator is running with cluster scope. + var watchNamespaceEnvVar = "WATCH_NAMESPACE" + + ns, _ := os.LookupEnv(watchNamespaceEnvVar) + return ns +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/internal/controller/testoperator_controller.go b/testdata/images/bundles/test-operator/v2.0.0/internal/controller/testoperator_controller.go new file mode 100644 index 000000000..ec4c6b3be --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/internal/controller/testoperator_controller.go @@ -0,0 +1,110 @@ +/* +Copyright 2025. + +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 controller + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/equality" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" + "sigs.k8s.io/controller-runtime/pkg/log" + + testolmv2 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/api/v2" +) + +const ( + peaceOutFinalizer = "olm.operatorframework.io/peace-out" +) + +// TestOperatorReconciler reconciles a TestOperator object +type TestOperatorReconciler struct { + client.Client + Finalizers crfinalizer.Finalizers +} + +// +kubebuilder:rbac:groups=testolm.operatorframework.io,resources=testoperators,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=testolm.operatorframework.io,resources=testoperators/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=testolm.operatorframework.io,resources=testoperators/finalizers,verbs=update + +func (r *TestOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx).WithName("test-operator") + ctx = log.IntoContext(ctx, l) + + l.Info("reconcile starting") + defer l.Info("reconcile ending") + + existingTestOp := &testolmv2.TestOperator{} + if err := r.Get(ctx, req.NamespacedName, existingTestOp); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Reconcile TestOperator + reconciledTestOp := existingTestOp.DeepCopy() + + // Reconcile finalizer + finalizeResult, err := r.Finalizers.Finalize(ctx, reconciledTestOp) + if err != nil { + return ctrl.Result{}, err + } + if finalizeResult.Updated || finalizeResult.StatusUpdated { + return ctrl.Result{}, r.Update(ctx, reconciledTestOp) + } + + // Reconcile status + reconciledTestOp.Status.Echo = reconciledTestOp.Spec.EchoMessage + + if !equality.Semantic.DeepEqual(existingTestOp.Status, reconciledTestOp.Status) { + return ctrl.Result{}, r.Client.Status().Update(ctx, reconciledTestOp) + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *TestOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := r.setupFinalizers(); err != nil { + return fmt.Errorf("failed to setup finalizers: %v", err) + } + return ctrl.NewControllerManagedBy(mgr). + For(&testolmv2.TestOperator{}). + Named("testoperator"). + Complete(r) +} + +type finalizerFunc func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) + +func (f finalizerFunc) Finalize(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { + return f(ctx, obj) +} + +func (r *TestOperatorReconciler) setupFinalizers() error { + f := crfinalizer.NewFinalizers() + err := f.Register(peaceOutFinalizer, finalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { + if _, ok := obj.(*testolmv2.TestOperator); !ok { + panic("could not convert object to testoperator") + } + log.FromContext(ctx).Info("peace out, bruh!") + return crfinalizer.Result{StatusUpdated: true}, nil + })) + if err != nil { + return err + } + r.Finalizers = f + return nil +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/internal/controller/testoperator_controller_test.go b/testdata/images/bundles/test-operator/v2.0.0/internal/controller/testoperator_controller_test.go new file mode 100644 index 000000000..9ada31016 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/internal/controller/testoperator_controller_test.go @@ -0,0 +1,138 @@ +/* +Copyright 2025. + +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 controller_test + +import ( + "context" + "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/internal/controller" + "github.com/stretchr/testify/require" + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" + "log" + "os" + "path/filepath" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + testolmv2 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/api/v2" +) + +var ( + config *rest.Config +) + +func TestMain(m *testing.M) { + testEnv := &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "manifests"), + }, + ErrorIfCRDPathMissing: true, + } + + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + + var err error + config, err = testEnv.Start() + utilruntime.Must(err) + if config == nil { + log.Panic("expected cfg to not be nil") + } + + code := m.Run() + utilruntime.Must(testEnv.Stop()) + os.Exit(code) +} + +// getFirstFoundEnvTestBinaryDir finds and returns the first directory under the given path. +func getFirstFoundEnvTestBinaryDir() string { + basePath := filepath.Join("..", "..", "..", "..", "..", "..", "..", "bin", "envtest-binaries", "k8s") + entries, _ := os.ReadDir(basePath) + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} + +func Test_Reconcile(t *testing.T) { + cl, reconciler := newClientAndReconciler(t) + + const resourceName = "test-resource" + + ctx := context.Background() + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + testoperator := &testolmv2.TestOperator{} + + err := cl.Get(ctx, typeNamespacedName, testoperator) + if err != nil && errors.IsNotFound(err) { + resource := &testolmv2.TestOperator{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: testolmv2.TestOperatorSpec{ + EchoMessage: "this is the message", + }, + } + require.NoError(t, cl.Create(ctx, resource)) + defer func() { + t.Log("Cleanup the specific resource instance TestOperator") + require.NoError(t, cl.Delete(ctx, resource)) + }() + } + + require.NoError(t, cl.Get(ctx, typeNamespacedName, testoperator)) + + t.Log("Reconciling the created resource") + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + require.NoError(t, err) + + t.Log("Checking that the resource was reconciled successfully") + err = cl.Get(ctx, typeNamespacedName, testoperator) + require.NoError(t, err) + require.Equal(t, testoperator.Status.Echo, "this is the message") +} + +func newClientAndReconciler(t *testing.T) (client.Client, *controller.TestOperatorReconciler) { + sch := apimachineryruntime.NewScheme() + require.NoError(t, testolmv2.AddToScheme(sch)) + cl, err := client.New(config, client.Options{Scheme: sch}) + require.NoError(t, err) + require.NotNil(t, cl) + + reconciler := &controller.TestOperatorReconciler{ + Client: cl, + Finalizers: crfinalizer.NewFinalizers(), + } + return cl, reconciler +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/internal/webhook/testoperator_webhook.go b/testdata/images/bundles/test-operator/v2.0.0/internal/webhook/testoperator_webhook.go new file mode 100644 index 000000000..5da0bffb7 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/internal/webhook/testoperator_webhook.go @@ -0,0 +1,120 @@ +/* +Copyright 2025. + +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 webhook + +import ( + "context" + "fmt" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + testolmv2 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/api/v2" +) + +const ( + DefaultMessageValue = "Echo" +) + +// nolint:unused +// log is for logging in this package. +var testoperatorlog = logf.Log.WithName("testoperator-resource") + +// SetupTestOperatorWebhookWithManager registers the webhook for TestOperator in the manager. +func SetupTestOperatorWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&testolmv2.TestOperator{}). + WithValidator(&TestOperatorCustomValidator{}). + WithDefaulter(&TestOperatorCustomDefaulter{}). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-testolm-operatorframework-io-v2-testoperator,mutating=true,failurePolicy=fail,sideEffects=None,groups=testolm.operatorframework.io,resources=testoperators,verbs=create;update,versions=v2,name=mtestoperator-v2.kb.io,admissionReviewVersions=v2 + +type TestOperatorCustomDefaulter struct{} + +var _ webhook.CustomDefaulter = &TestOperatorCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind TestOperator. +func (d *TestOperatorCustomDefaulter) Default(_ context.Context, obj runtime.Object) error { + testOp, ok := obj.(*testolmv2.TestOperator) + + if !ok { + return fmt.Errorf("expected an TestOperator object but got %T", obj) + } + testoperatorlog.Info("Defaulting for TestOperator", "name", testOp.GetName()) + + if len(strings.TrimSpace(testOp.Spec.EchoMessage)) == 0 { + testOp.Spec.EchoMessage = DefaultMessageValue + } + return nil +} + +// +kubebuilder:webhook:path=/validate-testolm-operatorframework-io-v2-testoperator,mutating=false,failurePolicy=fail,sideEffects=None,groups=testolm.operatorframework.io,resources=testoperators,verbs=create;update,versions=v2,name=vtestoperator-v2.kb.io,admissionReviewVersions=v2 + +type TestOperatorCustomValidator struct{} + +var _ webhook.CustomValidator = &TestOperatorCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type TestOperator. +func (v *TestOperatorCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + testOp, ok := obj.(*testolmv2.TestOperator) + if !ok { + return nil, fmt.Errorf("expected a TestOperator object but got %T", obj) + } + testoperatorlog.Info("Validation for TestOperator upon creation", "name", testOp.GetName()) + var allErrs field.ErrorList + if err := validateTestOperatorSpec(testOp); err != nil { + allErrs = append(allErrs, err) + } + if len(allErrs) == 0 { + return nil, nil + } + return nil, apierrors.NewInvalid( + schema.GroupKind{Group: testolmv2.GroupVersion.Group, Kind: "TestOperator"}, + testOp.GetName(), + allErrs, + ) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type TestOperator. +func (v *TestOperatorCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + testoperator, ok := newObj.(*testolmv2.TestOperator) + if !ok { + return nil, fmt.Errorf("expected a TestOperator object for the newObj but got %T", newObj) + } + testoperatorlog.Info("Validation for TestOperator upon update", "name", testoperator.GetName()) + return v.ValidateCreate(ctx, testoperator) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type TestOperator. +func (v *TestOperatorCustomValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +func validateTestOperatorSpec(testOp *testolmv2.TestOperator) *field.Error { + if strings.Contains(strings.ToLower(testOp.Spec.EchoMessage), "fight club") { + return field.Invalid(field.NewPath("spec").Child("echoMessage"), testOp.Spec.EchoMessage, "we DO NOT talk about fight club") + } + return nil +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/internal/webhook/testoperator_webhook_test.go b/testdata/images/bundles/test-operator/v2.0.0/internal/webhook/testoperator_webhook_test.go new file mode 100644 index 000000000..085dddfa3 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/internal/webhook/testoperator_webhook_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2025. + +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 webhook_test + +import ( + "context" + testolmv2 "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/api/v2" + "github.com/operator-framework/operator-controller/testdata/images/bundles/test-operator/v2.0.0/internal/webhook" + "github.com/stretchr/testify/require" + "testing" +) + +func Test_DefaultingWebhook(t *testing.T) { + obj := &testolmv2.TestOperator{} + defaulter := webhook.TestOperatorCustomDefaulter{} + t.Log("simulating a scenario where defaults should be applied") + obj.Spec.EchoMessage = "" + t.Log("calling the Default method to apply defaults") + require.NoError(t, defaulter.Default(context.Background(), obj)) + t.Log("checking that the default values are set") + require.Equal(t, "Echo", obj.Spec.EchoMessage) +} + +func Test_ValidatingWebhook(t *testing.T) { + validator := webhook.TestOperatorCustomValidator{} + + t.Log("checking creation validation") + obj := &testolmv2.TestOperator{} + obj.Spec.EchoMessage = "let's talk about fight club" + _, err := validator.ValidateCreate(context.Background(), obj) + require.Error(t, err) + require.Contains(t, err.Error(), "we DO NOT talk about fight club") + + t.Log("checking update validation") + t.Log("simulating a scenario where validating should be applied") + obj = &testolmv2.TestOperator{} + oldObj := &testolmv2.TestOperator{} + obj.Spec.EchoMessage = "let's talk about fight club" + _, err = validator.ValidateUpdate(context.Background(), oldObj, obj) + require.Error(t, err) + require.Contains(t, err.Error(), "we DO NOT talk about fight club") + + t.Log("checking there's no deletion validation") + obj = &testolmv2.TestOperator{} + obj.Spec.EchoMessage = "let's talk about fight club" + _, err = validator.ValidateDelete(context.Background(), obj) + require.NoError(t, err) +} diff --git a/testdata/images/bundles/test-operator/v2.0.0/manifests/bundle.configmap.yaml b/testdata/images/bundles/test-operator/v2.0.0/manifests/bundle.configmap.yaml deleted file mode 100644 index ef17ce45e..000000000 --- a/testdata/images/bundles/test-operator/v2.0.0/manifests/bundle.configmap.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: test-configmap - annotations: - shouldNotTemplate: > - The namespace is {{ $labels.namespace }}. The templated - $labels.namespace is NOT expected to be processed by OLM's - rendering engine for registry+v1 bundles. -data: - version: "v2.0.0" - name: "test-configmap" diff --git a/testdata/images/bundles/test-operator/v2.0.0/manifests/olm.operatorframework.com_olme2etest.yaml b/testdata/images/bundles/test-operator/v2.0.0/manifests/olm.operatorframework.com_olme2etest.yaml deleted file mode 100644 index fcfd4aeaf..000000000 --- a/testdata/images/bundles/test-operator/v2.0.0/manifests/olm.operatorframework.com_olme2etest.yaml +++ /dev/null @@ -1,28 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.1 - name: olme2etests.olm.operatorframework.io -spec: - group: olm.operatorframework.io - names: - kind: OLME2ETest - listKind: OLME2ETestList - plural: olme2etests - singular: olme2etest - scope: Cluster - versions: - - name: v1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - testField: - type: string diff --git a/testdata/images/bundles/test-operator/v2.0.0/manifests/test-operator.v2.0.0.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v2.0.0/manifests/test-operator.v2.0.0.clusterserviceversion.yaml new file mode 100644 index 000000000..283fa9ae0 --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/manifests/test-operator.v2.0.0.clusterserviceversion.yaml @@ -0,0 +1,252 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + createdAt: "2025-07-04T08:55:46Z" + operators.operatorframework.io/builder: operator-sdk-v1.40.0+git + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: test-operator.v2.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: TestOperator is the Schema for the testoperators API. + displayName: Test Operator + kind: TestOperator + name: testoperators.testolm.operatorframework.io + version: v1 + - description: TestOperator is the Schema for the testoperators API. + displayName: Test Operator + kind: TestOperator + name: testoperators.testolm.operatorframework.io + version: v2 + description: OLM Test Operator + displayName: OLM Test Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators/finalizers + verbs: + - update + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators/status + verbs: + - get + - patch + - update + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: test-operator-controller-manager + deployments: + - label: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: test-operator + control-plane: controller-manager + name: test-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: test-operator + control-plane: controller-manager + strategy: {} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + app.kubernetes.io/name: test-operator + control-plane: controller-manager + spec: + containers: + - args: + - --leader-elect + - --health-probe-bind-address=:8081 + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + command: + - /manager + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.annotations['olm.targetNamespaces'] + image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/controllers/test-operator:v2.0.0 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: test-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: test-operator-controller-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - test + - operator + links: + - name: V1 + url: https://github.com/operator-framework/operator-controller + maintainers: + - email: operator-framework-olm-dev@googlegroups.com + name: community + maturity: alpha + provider: + name: operator-framework + url: https://github.com/operator-framework/operator-controller + version: 2.0.0 + webhookdefinitions: + - admissionReviewVersions: + - v1 + containerPort: 443 + conversionCRDs: + - testoperators.testolm.operatorframework.io + deploymentName: test-operator-controller-manager + generateName: ctestoperators.kb.io + sideEffects: None + targetPort: 9443 + type: ConversionWebhook + webhookPath: /convert + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: test-operator-controller-manager + failurePolicy: Fail + generateName: mtestoperator-v2.kb.io + rules: + - apiGroups: + - testolm.operatorframework.io + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - testoperators + sideEffects: None + targetPort: 9443 + type: MutatingAdmissionWebhook + webhookPath: /mutate-testolm-operatorframework-io-v2-testoperator + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: test-operator-controller-manager + failurePolicy: Fail + generateName: vtestoperator-v2.kb.io + rules: + - apiGroups: + - testolm.operatorframework.io + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - testoperators + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-testolm-operatorframework-io-v2-testoperator diff --git a/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperator.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperator.clusterserviceversion.yaml deleted file mode 100644 index a375c1901..000000000 --- a/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperator.clusterserviceversion.yaml +++ /dev/null @@ -1,141 +0,0 @@ -apiVersion: operators.coreos.com/v1alpha1 -kind: ClusterServiceVersion -metadata: - annotations: - alm-examples: |- - [ - { - "apiVersion": "olme2etests.olm.operatorframework.io/v1", - "kind": "OLME2ETests", - "metadata": { - "labels": { - "app.kubernetes.io/managed-by": "kustomize", - "app.kubernetes.io/name": "test" - }, - "name": "test-sample" - }, - "spec": null - } - ] - capabilities: Basic Install - createdAt: "2024-10-24T19:21:40Z" - operators.operatorframework.io/builder: operator-sdk-v1.34.1 - operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 - name: testoperator.v2.0.0 - namespace: placeholder -spec: - apiservicedefinitions: {} - customresourcedefinitions: - owned: - - description: Configures subsections of Alertmanager configuration specific to each namespace - displayName: OLME2ETest - kind: OLME2ETest - name: olme2etests.olm.operatorframework.io - version: v1 - description: OLM E2E Testing Operator - displayName: test-operator - icon: - - base64data: "" - mediatype: "" - install: - spec: - deployments: - - label: - app.kubernetes.io/component: controller - app.kubernetes.io/name: test-operator - app.kubernetes.io/version: 2.0.0 - name: test-operator - spec: - replicas: 1 - selector: - matchLabels: - app: olme2etest - template: - metadata: - labels: - app: olme2etest - spec: - terminationGracePeriodSeconds: 0 - containers: - - name: busybox - image: busybox - command: - - 'sleep' - - '1000' - securityContext: - runAsUser: 1000 - runAsNonRoot: true - serviceAccountName: simple-bundle-manager - clusterPermissions: - - rules: - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create - serviceAccountName: simple-bundle-manager - permissions: - - rules: - - apiGroups: - - "" - resources: - - configmaps - - serviceaccounts - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch - serviceAccountName: simple-bundle-manager - strategy: deployment - installModes: - - supported: false - type: OwnNamespace - - supported: false - type: SingleNamespace - - supported: false - type: MultiNamespace - - supported: true - type: AllNamespaces - keywords: - - registry - links: - - name: simple-bundle - url: https://simple-bundle.domain - maintainers: - - email: main#simple-bundle.domain - name: Simple Bundle - maturity: beta - provider: - name: Simple Bundle - url: https://simple-bundle.domain - version: 2.0.0 diff --git a/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml b/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml new file mode 100644 index 000000000..2ab765aef --- /dev/null +++ b/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml @@ -0,0 +1,110 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + creationTimestamp: null + name: testoperators.testolm.operatorframework.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: test-operator-webhook-service + namespace: test-operator-system + path: /convert + conversionReviewVersions: + - v1 + group: testolm.operatorframework.io + names: + kind: TestOperator + listKind: TestOperatorList + plural: testoperators + singular: testoperator + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: TestOperator is the Schema for the testoperators 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: TestOperatorSpec defines the desired state of TestOperator. + properties: + message: + type: string + type: object + status: + description: TestOperatorStatus defines the observed state of TestOperator. + properties: + echo: + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} + - name: v2 + schema: + openAPIV3Schema: + description: TestOperator is the Schema for the testoperators 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: TestOperatorSpec defines the desired state of TestOperator. + properties: + echoMessage: + type: string + type: object + status: + description: TestOperatorStatus defines the observed state of TestOperator. + properties: + echo: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/testdata/images/bundles/test-operator/v2.0.0/metadata/annotations.yaml b/testdata/images/bundles/test-operator/v2.0.0/metadata/annotations.yaml index 404f0f4a3..8ba0e750b 100644 --- a/testdata/images/bundles/test-operator/v2.0.0/metadata/annotations.yaml +++ b/testdata/images/bundles/test-operator/v2.0.0/metadata/annotations.yaml @@ -1,10 +1,6 @@ annotations: - # Core bundle annotations. operators.operatorframework.io.bundle.mediatype.v1: registry+v1 operators.operatorframework.io.bundle.manifests.v1: manifests/ operators.operatorframework.io.bundle.metadata.v1: metadata/ operators.operatorframework.io.bundle.package.v1: test operators.operatorframework.io.bundle.channels.v1: beta - operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.0 - operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 - operators.operatorframework.io.metrics.project_layout: unknown From 13a56d3df51a5e8686ddf4b38b8f3b14a1871a19 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 8 Jul 2025 15:49:54 +0200 Subject: [PATCH 3/4] Move v2.0.0 e2e test to v1.3.0 due to lack of webhook support Signed-off-by: Per Goncalves da Silva --- test/e2e/cluster_extension_install_test.go | 2 +- .../v1.3.0/manifests/bundle.configmap.yaml | 12 ++ ...operator.v1.0.0.clusterserviceversion.yaml | 190 ++++++++++++++++++ ...tors.testolm.operatorframework.io.crd.yaml | 61 ++++++ .../v1.3.0/metadata/annotations.yaml | 6 + .../test-catalog/v2/configs/catalog.yaml | 8 +- 6 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 testdata/images/bundles/test-operator/v1.3.0/manifests/bundle.configmap.yaml create mode 100644 testdata/images/bundles/test-operator/v1.3.0/manifests/test-operator.v1.0.0.clusterserviceversion.yaml create mode 100644 testdata/images/bundles/test-operator/v1.3.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml create mode 100644 testdata/images/bundles/test-operator/v1.3.0/metadata/annotations.yaml diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go index bc82512b9..038c90b36 100644 --- a/test/e2e/cluster_extension_install_test.go +++ b/test/e2e/cluster_extension_install_test.go @@ -732,7 +732,7 @@ func TestClusterExtensionInstallReResolvesWhenCatalogIsPatched(t *testing.T) { assert.Equal(ct, metav1.ConditionTrue, cond.Status) assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) assert.Contains(ct, cond.Message, "Installed bundle") - assert.Contains(ct, clusterExtension.Status.Install.Bundle.Version, "2.0.0") + assert.Contains(ct, clusterExtension.Status.Install.Bundle.Version, "1.3.0") } }, pollDuration, pollInterval) diff --git a/testdata/images/bundles/test-operator/v1.3.0/manifests/bundle.configmap.yaml b/testdata/images/bundles/test-operator/v1.3.0/manifests/bundle.configmap.yaml new file mode 100644 index 000000000..390489760 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.3.0/manifests/bundle.configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap + annotations: + shouldNotTemplate: > + The namespace is {{ $labels.namespace }}. The templated + $labels.namespace is NOT expected to be processed by OLM's + rendering engine for registry+v1 bundles. +data: + version: "v1.3.0" + name: "test-configmap" diff --git a/testdata/images/bundles/test-operator/v1.3.0/manifests/test-operator.v1.0.0.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v1.3.0/manifests/test-operator.v1.0.0.clusterserviceversion.yaml new file mode 100644 index 000000000..ff9963703 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.3.0/manifests/test-operator.v1.0.0.clusterserviceversion.yaml @@ -0,0 +1,190 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + createdAt: "2025-07-03T15:15:31Z" + operators.operatorframework.io/builder: operator-sdk-v1.40.0+git + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: test-operator.v1.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: TestOperator is the Schema for the testoperators API. + displayName: Test Operator + kind: TestOperator + name: testoperators.testolm.operatorframework.io + version: v1 + description: Test OLM Operator + displayName: OLM Test Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators/finalizers + verbs: + - update + - apiGroups: + - testolm.operatorframework.io + resources: + - testoperators/status + verbs: + - get + - patch + - update + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: test-operator-controller-manager + deployments: + - label: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: test-operator + control-plane: controller-manager + name: test-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: test-operator + control-plane: controller-manager + strategy: {} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + app.kubernetes.io/name: test-operator + control-plane: controller-manager + spec: + containers: + - args: + - --leader-elect + - --health-probe-bind-address=:8081 + command: + - /manager + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.annotations['olm.targetNamespaces'] + image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/controllers/test-operator:v1.0.0 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: test-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: test-operator-controller-manager + strategy: deployment + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - test + - operator + links: + - name: V1 + url: https://github.com/operator-framework/operator-controller + maintainers: + - email: operator-framework-olm-dev@googlegroups.com + name: community + maturity: alpha + provider: + name: operator-framework + url: https://github.com/operator-framework/operator-controller + version: 1.0.0 diff --git a/testdata/images/bundles/test-operator/v1.3.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml b/testdata/images/bundles/test-operator/v1.3.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml new file mode 100644 index 000000000..685c165ea --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.3.0/manifests/testoperators.testolm.operatorframework.io.crd.yaml @@ -0,0 +1,61 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + creationTimestamp: null + name: testoperators.testolm.operatorframework.io +spec: + group: testolm.operatorframework.io + names: + kind: TestOperator + listKind: TestOperatorList + plural: testoperators + singular: testoperator + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: TestOperator is the Schema for the testoperators 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: TestOperatorSpec defines the desired state of TestOperator. + properties: + message: + type: string + type: object + status: + description: TestOperatorStatus defines the observed state of TestOperator. + properties: + echo: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/testdata/images/bundles/test-operator/v1.3.0/metadata/annotations.yaml b/testdata/images/bundles/test-operator/v1.3.0/metadata/annotations.yaml new file mode 100644 index 000000000..8ba0e750b --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.3.0/metadata/annotations.yaml @@ -0,0 +1,6 @@ +annotations: + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: test + operators.operatorframework.io.bundle.channels.v1: beta diff --git a/testdata/images/catalogs/test-catalog/v2/configs/catalog.yaml b/testdata/images/catalogs/test-catalog/v2/configs/catalog.yaml index 821e7631b..23c3df180 100644 --- a/testdata/images/catalogs/test-catalog/v2/configs/catalog.yaml +++ b/testdata/images/catalogs/test-catalog/v2/configs/catalog.yaml @@ -7,15 +7,15 @@ schema: olm.channel name: beta package: test entries: - - name: test-operator.2.0.0 + - name: test-operator.1.3.0 replaces: test-operator.1.2.0 --- schema: olm.bundle -name: test-operator.2.0.0 +name: test-operator.1.3.0 package: test -image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v2.0.0 +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.3.0 properties: - type: olm.package value: packageName: test - version: 2.0.0 + version: 1.3.0 From 2396e27a519fedba6ba656ac88a2be041036971f Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 8 Jul 2025 17:14:25 +0200 Subject: [PATCH 4/4] Add controller image build and publish support to test registry Signed-off-by: Per Goncalves da Silva --- Makefile | 9 +++- testdata/.gitignore | 1 + testdata/push/push.go | 97 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 3e81a773b..1e10922bf 100644 --- a/Makefile +++ b/Makefile @@ -246,11 +246,18 @@ test-unit: $(SETUP_ENVTEST) envtest-k8s-bins #HELP Run the unit tests $(UNIT_TEST_DIRS) \ -test.gocoverdir=$(COVERAGE_UNIT_DIR) +TEST_OPERATOR_CONTROLLERS_HOME=./testdata/images/controllers +TEST_OPERATOR_CONTROLLERS=v1.0.0 v2.0.0 + +.PHONY: $(TEST_OPERATOR_CONTROLLERS) +$(TEST_OPERATOR_CONTROLLERS): + 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 $(TEST_OPERATOR_CONTROLLERS_HOME)/test-operator/$@/manager ./testdata/images/bundles/test-operator/$@/cmd/main.go + .PHONY: image-registry E2E_REGISTRY_IMAGE=localhost/e2e-test-registry:devel image-registry: export GOOS=linux image-registry: export GOARCH=amd64 -image-registry: ## Build the testdata catalog used for e2e tests and push it to the image registry +image-registry: $(TEST_OPERATOR_CONTROLLERS) ## Build the testdata catalog used for e2e tests and push it to the image registry 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 ./testdata/push/bin/push ./testdata/push/push.go $(CONTAINER_RUNTIME) build -f ./testdata/Dockerfile -t $(E2E_REGISTRY_IMAGE) ./testdata $(CONTAINER_RUNTIME) save $(E2E_REGISTRY_IMAGE) | $(KIND) load image-archive /dev/stdin --name $(KIND_CLUSTER_NAME) diff --git a/testdata/.gitignore b/testdata/.gitignore index 8e0dcaba1..d8da05951 100644 --- a/testdata/.gitignore +++ b/testdata/.gitignore @@ -1 +1,2 @@ push/bin +images/controllers diff --git a/testdata/push/push.go b/testdata/push/push.go index 13a029eda..252eae451 100644 --- a/testdata/push/push.go +++ b/testdata/push/push.go @@ -1,23 +1,31 @@ package main import ( + "archive/tar" + "bytes" "flag" "fmt" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "io" "io/fs" "log" "os" + "path/filepath" + "sort" "strings" "github.com/google/go-containerregistry/pkg/crane" - v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/spf13/pflag" "gopkg.in/yaml.v2" ) const ( - bundlesSubPath string = "bundles" - catalogsSubPath string = "catalogs" + controllersSubPath string = "controllers" + bundlesSubPath string = "bundles" + catalogsSubPath string = "catalogs" ) func main() { @@ -34,6 +42,7 @@ func main() { bundlesFullPath := fmt.Sprintf("%s/%s", imagesPath, bundlesSubPath) catalogsFullPath := fmt.Sprintf("%s/%s", imagesPath, catalogsSubPath) + controllersFullPath := fmt.Sprintf("%s/%s", imagesPath, controllersSubPath) bundles, err := buildBundles(bundlesFullPath) if err != nil { @@ -43,6 +52,10 @@ func main() { if err != nil { log.Fatalf("failed to build catalogs: %s", err.Error()) } + controllers, err := buildControllers(controllersFullPath) + if err != nil { + log.Fatalf("failed to build controllers: %s", err.Error()) + } // Push the images for name, image := range bundles { dest := fmt.Sprintf("%s/%s", registryAddr, name) @@ -58,6 +71,13 @@ func main() { log.Fatalf("failed to push catalog images: %s", err.Error()) } } + for name, image := range controllers { + dest := fmt.Sprintf("%s/%s", registryAddr, name) + log.Printf("pushing controller %s to %s", name, dest) + if err := crane.Push(image, dest); err != nil { + log.Fatalf("failed to push controller images: %s", err.Error()) + } + } log.Printf("finished") os.Exit(0) } @@ -122,6 +142,27 @@ func buildCatalogs(path string) (map[string]v1.Image, error) { return mutatedMap, nil } +func buildControllers(path string) (map[string]v1.Image, error) { + controllers, err := processImageDirTree(path) + if err != nil { + return nil, err + } + mutatedMap := make(map[string]v1.Image, 0) + // Apply required catalog label + for key, img := range controllers { + cfg := v1.Config{ + WorkingDir: "/", + Entrypoint: []string{"/manager"}, + User: "65532:65532", + } + mutatedMap[fmt.Sprintf("controllers/%s", key)], err = mutate.Config(img, cfg) + if err != nil { + return nil, fmt.Errorf("failed to apply image labels: %w", err) + } + } + return mutatedMap, nil +} + func processImageDirTree(path string) (map[string]v1.Image, error) { imageMap := make(map[string]v1.Image, 0) images, err := os.ReadDir(path) @@ -145,16 +186,47 @@ func processImageDirTree(path string) (map[string]v1.Image, error) { continue } tagFullPath := fmt.Sprintf("%s/%s", entryFullPath, tag.Name()) + b := &bytes.Buffer{} + w := tar.NewWriter(b) - var fileMap map[string][]byte - fileMap, err = createFileMap(tagFullPath) + files, err := collectFiles(tagFullPath) if err != nil { return nil, fmt.Errorf("failed to read files for image: %w", err) } + sort.Strings(files) + + for _, f := range files { + filePath := filepath.Join(tagFullPath, f) + fileBytes, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file %q for image: %w", filePath, err) + } + if err := w.WriteHeader(&tar.Header{ + Name: f, + Mode: 0755, + Size: int64(len(fileBytes)), + }); err != nil { + return nil, err + } + if _, err := w.Write(fileBytes); err != nil { + return nil, err + } + } + if err := w.Close(); err != nil { + return nil, err + } + + // Return a new copy of the buffer each time it's opened. + layer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(b.Bytes())), nil + }) + if err != nil { + return nil, fmt.Errorf("failed to create image layer: %w", err) + } - image, err := crane.Image(fileMap) + image, err := mutate.AppendLayers(empty.Image, layer) if err != nil { - return nil, fmt.Errorf("failed to generate image: %w", err) + return nil, fmt.Errorf("failed to append layer to image: %w", err) } imageMap[fmt.Sprintf("%s:%s", entry.Name(), tag.Name())] = image } @@ -162,21 +234,18 @@ func processImageDirTree(path string) (map[string]v1.Image, error) { return imageMap, nil } -func createFileMap(originPath string) (map[string][]byte, error) { - fileMap := make(map[string][]byte) +func collectFiles(originPath string) ([]string, error) { + var files []string if err := fs.WalkDir(os.DirFS(originPath), ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d != nil && !d.IsDir() { - fileMap[path], err = os.ReadFile(fmt.Sprintf("%s/%s", originPath, path)) - if err != nil { - return err - } + files = append(files, path) } return nil }); err != nil { return nil, err } - return fileMap, nil + return files, nil }