diff --git a/cmd/manager/main.go b/cmd/manager/main.go index d5da75c32..7fe437f69 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -23,14 +23,18 @@ import ( "net/http" "os" "path/filepath" + "strings" "time" "github.com/containers/image/v5/types" "github.com/go-logr/logr" "github.com/spf13/pflag" + corev1 "k8s.io/api/core/v1" apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + "k8s.io/apimachinery/pkg/fields" k8slabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" + k8stypes "k8s.io/apimachinery/pkg/types" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/klog/v2" @@ -68,7 +72,7 @@ var ( defaultSystemNamespace = "olmv1-system" ) -const authFilePath = "/etc/operator-controller/auth.json" +const authFilePath = "/tmp/operator-controller/auth.json" // podNamespace checks whether the controller is running in a Pod vs. // being run locally by inspecting the namespace file that gets mounted @@ -91,6 +95,7 @@ func main() { operatorControllerVersion bool systemNamespace string caCertDir string + globalPullSecret string ) flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -101,6 +106,7 @@ func main() { flag.StringVar(&cachePath, "cache-path", "/var/cache", "The local directory path used for filesystem based caching") flag.BoolVar(&operatorControllerVersion, "version", false, "Prints operator-controller version information") flag.StringVar(&systemNamespace, "system-namespace", "", "Configures the namespace that gets used to deploy system resources.") + flag.StringVar(&globalPullSecret, "global-pull-secret", "", "The namespace/name of the global pull secret that is going to be used to pull bundle images.") klog.InitFlags(flag.CommandLine) @@ -117,6 +123,16 @@ func main() { setupLog.Info("starting up the controller", "version info", version.String()) + var globalPullSecretKey *k8stypes.NamespacedName + if globalPullSecret != "" { + secretParts := strings.Split(globalPullSecret, "/") + if len(secretParts) != 2 { + setupLog.Error(fmt.Errorf("incorrect number of components"), "value of global-pull-secret should be of the format /") + os.Exit(1) + } + globalPullSecretKey = &k8stypes.NamespacedName{Name: secretParts[1], Namespace: secretParts[0]} + } + if systemNamespace == "" { systemNamespace = podNamespace() } @@ -129,22 +145,33 @@ func main() { dependentSelector := k8slabels.NewSelector().Add(*dependentRequirement) setupLog.Info("set up manager") + cacheOptions := crcache.Options{ + ByObject: map[client.Object]crcache.ByObject{ + &ocv1alpha1.ClusterExtension{}: {Label: k8slabels.Everything()}, + &catalogd.ClusterCatalog{}: {Label: k8slabels.Everything()}, + }, + DefaultNamespaces: map[string]crcache.Config{ + systemNamespace: {LabelSelector: k8slabels.Everything()}, + }, + DefaultLabelSelector: dependentSelector, + } + if globalPullSecretKey != nil { + cacheOptions.ByObject[&corev1.Secret{}] = crcache.ByObject{ + Field: fields.SelectorFromSet(map[string]string{ + "metadata.name": globalPullSecretKey.Name, + }), + } + cacheOptions.DefaultNamespaces[globalPullSecretKey.Namespace] = crcache.Config{ + LabelSelector: k8slabels.Everything(), + } + } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme.Scheme, Metrics: server.Options{BindAddress: metricsAddr}, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "9c4404e7.operatorframework.io", - Cache: crcache.Options{ - ByObject: map[client.Object]crcache.ByObject{ - &ocv1alpha1.ClusterExtension{}: {Label: k8slabels.Everything()}, - &catalogd.ClusterCatalog{}: {Label: k8slabels.Everything()}, - }, - DefaultNamespaces: map[string]crcache.Config{ - systemNamespace: {LabelSelector: k8slabels.Everything()}, - }, - DefaultLabelSelector: dependentSelector, - }, + Cache: cacheOptions, // 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 @@ -289,6 +316,19 @@ func main() { os.Exit(1) } + if globalPullSecretKey != nil { + setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", globalPullSecret) + err := (&controllers.SecretSyncerReconciler{ + Client: mgr.GetClient(), + AuthFilePath: authFilePath, + SecretKey: *globalPullSecretKey, + }).SetupWithManager(mgr) + if err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") + os.Exit(1) + } + } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/internal/controllers/secret_syncer_controller.go b/internal/controllers/secret_syncer_controller.go new file mode 100644 index 000000000..bc683ca64 --- /dev/null +++ b/internal/controllers/secret_syncer_controller.go @@ -0,0 +1,125 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// SecretSyncerReconciler reconciles a specific secret object +type SecretSyncerReconciler struct { + client.Client + SecretKey types.NamespacedName + AuthFilePath string +} + +func (r *SecretSyncerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + if req.Name != r.SecretKey.Name || req.Namespace != r.SecretKey.Namespace { + logger.Info("received unexpected request for Secret") + return ctrl.Result{}, nil + } + + secret := &corev1.Secret{} + err := r.Get(ctx, req.NamespacedName, secret) + if err != nil { + if apierrors.IsNotFound(err) { + logger.Info("secret not found") + return r.deleteSecretFile(logger) + } + logger.Error(err, "failed to get Secret") + return ctrl.Result{}, err + } + + return r.writeSecretToFile(logger, secret) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *SecretSyncerReconciler) SetupWithManager(mgr ctrl.Manager) error { + _, err := ctrl.NewControllerManagedBy(mgr). + For(&corev1.Secret{}). + WithEventFilter(newSecretPredicate(r.SecretKey)). + Build(r) + + return err +} + +func newSecretPredicate(key types.NamespacedName) predicate.Predicate { + return predicate.NewPredicateFuncs(func(obj client.Object) bool { + return obj.GetName() == key.Name && obj.GetNamespace() == key.Namespace + }) +} + +// writeSecretToFile writes the secret data to the specified file +func (r *SecretSyncerReconciler) writeSecretToFile(logger logr.Logger, secret *corev1.Secret) (ctrl.Result, error) { + for key, value := range secret.Data { + // image registry secrets are always stored with the key .dockerconfigjson + // ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials + if key == ".dockerconfigjson" { + // expected format for auth.json + // https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md + if err := r.writeDataToFile(value); err != nil { + return ctrl.Result{}, err + } + logger.Info("saved Secret data locally", "file", r.AuthFilePath) + } + } + return ctrl.Result{}, nil +} + +// deleteSecretFile deletes the auth file if the secret is deleted +func (r *SecretSyncerReconciler) deleteSecretFile(logger logr.Logger) (ctrl.Result, error) { + logger.Info("deleting local auth file.", "file", r.AuthFilePath) + if _, err := os.Stat(r.AuthFilePath); err == nil { + err := os.Remove(r.AuthFilePath) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to delete secret file: %w", err) + } + logger.Info("auth file deleted successfully", "file", r.AuthFilePath) + } else if os.IsNotExist(err) { + logger.Info("auth file does not exist, nothing to delete", "file", r.AuthFilePath) + } else { + return ctrl.Result{}, fmt.Errorf("failed to check secret file: %w", err) + } + return ctrl.Result{}, nil +} + +func (r *SecretSyncerReconciler) writeDataToFile(data []byte) error { + // create the directory first if it does not exists + dir := filepath.Dir(r.AuthFilePath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("unable to create directory for storing auth: %w", err) + + } + err := os.WriteFile(r.AuthFilePath, data, 0644) + if err != nil { + return fmt.Errorf("failed to write secret data to file: %w", err) + } + return nil +}