pkg/operator/controllers/pullsecret/pullsecret_controller.go (159 lines of code) (raw):

package pullsecret // Copyright (c) Microsoft Corporation. // Licensed under the Apache License 2.0. // Image Registry pull-secret reconciler // Users tend to do damage to corev1.Secret openshift-config/pull-secret // this controllers ensures valid ARO secret for Azure mirror with // openshift images // It also signals presense of Red Hat image registry keys in a // cluster.status.RedHatKeysPresent field. import ( "context" "encoding/json" "errors" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "github.com/Azure/ARO-RP/pkg/operator" arov1alpha1 "github.com/Azure/ARO-RP/pkg/operator/apis/aro.openshift.io/v1alpha1" "github.com/Azure/ARO-RP/pkg/operator/controllers/base" "github.com/Azure/ARO-RP/pkg/operator/predicates" "github.com/Azure/ARO-RP/pkg/util/pullsecret" ) const ( ControllerName = "PullSecret" ) var pullSecretName = types.NamespacedName{Name: "pull-secret", Namespace: "openshift-config"} var rhKeys = []string{"registry.redhat.io", "cloud.openshift.com", "registry.connect.redhat.com"} // Reconciler reconciles a Cluster object type Reconciler struct { base.AROController } func NewReconciler(log *logrus.Entry, client client.Client) *Reconciler { return &Reconciler{ AROController: base.AROController{ Log: log, Client: client, Name: ControllerName, }, } } // Reconcile will make sure that the ACR part of the pull secret is correct. The // conditions under which Reconcile is called are slightly unusual and are as // follows: // - If the Cluster object changes, we'll see the *Cluster* object requested. // - If a Secret object owned by the Cluster object changes (e.g., but not // limited to, the configuration Secret, we'll see the *Cluster* object // requested). // - If the pull Secret object (which is not owned by the Cluster object) // changes, we'll see the pull Secret object requested. func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { instance, err := r.GetCluster(ctx) if err != nil { r.Log.Error(err) return reconcile.Result{}, err } if !instance.Spec.OperatorFlags.GetSimpleBoolean(operator.PullSecretEnabled) { r.Log.Debug("controller is disabled") return reconcile.Result{}, nil } r.Log.Debug("running") userSecret := &corev1.Secret{} err = r.Client.Get(ctx, pullSecretName, userSecret) if err != nil && !kerrors.IsNotFound(err) { r.Log.Error(err) return reconcile.Result{}, err } // reconcile global pull secret // detects if the global pull secret is broken and fixes it by using backup managed by ARO operator if instance.Spec.OperatorFlags.GetSimpleBoolean(operator.PullSecretManaged) { operatorSecret := &corev1.Secret{} err = r.Client.Get(ctx, types.NamespacedName{Namespace: operator.Namespace, Name: operator.SecretName}, operatorSecret) if err != nil { r.Log.Error(err) return reconcile.Result{}, err } // fix pull secret if its broken to have at least the ARO pull secret userSecret, err = r.ensureGlobalPullSecret(ctx, operatorSecret, userSecret) if err != nil { r.Log.Error(err) return reconcile.Result{}, err } } // reconcile cluster status // update the following information: // - list of Red Hat pull-secret keys in status. instance.Status.RedHatKeysPresent, err = r.parseRedHatKeys(userSecret) if err != nil { r.Log.Error(err) return reconcile.Result{}, err } err = r.Client.Update(ctx, instance) if err == nil { r.ClearConditions(ctx) } else { r.SetDegraded(ctx, err) } return reconcile.Result{}, err } // SetupWithManager setup our manager func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&arov1alpha1.Cluster{}, builder.WithPredicates(predicate.And(predicates.AROCluster, predicate.GenerationChangedPredicate{}))). // https://github.com/kubernetes-sigs/controller-runtime/issues/1173 // equivalent to For(&v1.Secret{})., but can't call For multiple times on one builder Watches( &source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(predicate.Or(predicates.PullSecret, predicates.BackupPullSecret)), ). Named(ControllerName). Complete(r) } // ensureGlobalPullSecret checks the state of the pull secrets, in case of missing or broken ARO pull secret // it replaces it with working one from controller Secret // it takes care only for ARO pull secret, it does not touch the customer keys func (r *Reconciler) ensureGlobalPullSecret(ctx context.Context, operatorSecret, userSecret *corev1.Secret) (secret *corev1.Secret, err error) { if operatorSecret == nil { return nil, errors.New("nil operator secret, cannot verify userData integrity") } recreate := false // if there is no userSecret, create new, or when // userSecret have broken type, recreates it with proper type // unfortunately the type field is immutable, therefore the whole secret have to be deleted and create once more if userSecret == nil || (userSecret.Type != corev1.SecretTypeDockerConfigJson || userSecret.Data == nil) { recreate = true } if recreate { secret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: pullSecretName.Name, Namespace: pullSecretName.Namespace, }, Type: corev1.SecretTypeDockerConfigJson, Data: make(map[string][]byte), } } else { secret = userSecret.DeepCopy() if !json.Valid(secret.Data[corev1.DockerConfigJsonKey]) { delete(secret.Data, corev1.DockerConfigJsonKey) } } fixedData, update, err := pullsecret.Merge(string(secret.Data[corev1.DockerConfigJsonKey]), string(operatorSecret.Data[corev1.DockerConfigJsonKey])) if err != nil { return nil, err } // update is true for any case when ARO keys are fixed, meaning no need to double check for recreation if !update { return userSecret, nil } secret.Data[corev1.DockerConfigJsonKey] = []byte(fixedData) if recreate { // delete possible existing userSecret, calling deletion every time and ignoring when secret not found // allows for simpler logic flow, when delete and create are not handled separately // this call happens only when there is a need to change, it has no significant impact on performance err := r.Client.Delete(ctx, secret) r.Log.Info("Global Pull secret Not Found, Creating Again") if err != nil && !kerrors.IsNotFound(err) { r.Log.Error(err) return nil, err } err = r.Client.Create(ctx, secret) if err == nil { r.Log.Info("Global Pull secret Created") } return secret, err } err = r.Client.Update(ctx, secret) if err == nil { r.Log.Info("Updated Existing Global Pull secret") } return secret, err } // parseRedHatKeys unmarshal and extract following RH keys from pull-secret: // - redhat.registry.io // - cloud.openshift.com // - registry.connect.redhat.com // // if present, return error when the parsing fail, which means broken secret func (r *Reconciler) parseRedHatKeys(secret *corev1.Secret) (foundKeys []string, err error) { // parse keys and validate JSON parsedKeys, err := pullsecret.UnmarshalSecretData(secret) if err != nil { r.Log.Info("pull secret is not valid json - recreating") return foundKeys, err } if parsedKeys != nil { for _, rhKey := range rhKeys { if v := parsedKeys[rhKey]; len(v) > 0 { foundKeys = append(foundKeys, rhKey) } } } return foundKeys, nil }