package controllers import ( "context" "errors" "fmt" "maps" "slices" "strings" "time" "github.com/rancher/scc-operator/internal/rancher" "github.com/rancher/scc-operator/internal/rancher/settings" "github.com/rancher/scc-operator/internal/telemetry" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/util/retry" "github.com/rancher/scc-operator/internal/consts" "github.com/rancher/scc-operator/internal/initializer" "github.com/rancher/scc-operator/internal/repos/secretrepo" "github.com/rancher/scc-operator/internal/suseconnect" "github.com/rancher/scc-operator/internal/suseconnect/credentials" "github.com/rancher/scc-operator/internal/suseconnect/offline" "github.com/rancher/scc-operator/internal/types" wranglerPolyfill "github.com/rancher/scc-operator/internal/wrangler/polyfill" v1 "github.com/rancher/scc-operator/pkg/apis/scc.cattle.io/v1" "github.com/rancher/scc-operator/pkg/controllers/helpers" "github.com/rancher/scc-operator/pkg/controllers/shared" registrationControllers "github.com/rancher/scc-operator/pkg/generated/controllers/scc.cattle.io/v1" "github.com/rancher/scc-operator/pkg/util/log" ) const ( controllerID = "prime-registration" prodBaseCheckin = time.Hour * 20 prodMinCheckin = prodBaseCheckin - (3 * time.Hour) devBaseCheckin = time.Minute * 30 devMinCheckin = devBaseCheckin - (10 * time.Minute) ) // SCCHandler Defines a common interface for online and offline operations // IMPORTANT: All the `Reconcile*` methods modifies the object in memory but does NOT save it. The caller is responsible for saving the state. type SCCHandler interface { // SetRancherMetrics adds the current Rancher metrics to the SCCHandler for processing // This must be added after initialization so that we only fetch metrics if we need to sync. SetRancherMetrics(rancherMetrics telemetry.MetricsWrapper) // NeedsRegistration determines if the system requires initial SCC registration. NeedsRegistration(*v1.Registration) bool // NeedsActivation checks if the system requires activation with SCC. NeedsActivation(*v1.Registration) bool // ReadyForActivation checks if the system is ready for activation. ReadyForActivation(*v1.Registration) bool // ResetToRegisteredForActivation will clean up the registration back to the ReadyForActivation state ResetToRegisteredForActivation(*v1.Registration) (*v1.Registration, error) // PrepareForRegister preforms pre-registration steps PrepareForRegister(*v1.Registration) (*v1.Registration, error) // Register performs the initial system registration with SCC or creates an offline request. Register(*v1.Registration) (suseconnect.RegistrationSystemID, error) // PrepareRegisteredForActivation prepares the Registration object after successful registration. PrepareRegisteredForActivation(*v1.Registration) (*v1.Registration, error) // Activate activates the system with SCC or verifies an offline request. Activate(*v1.Registration) error // PrepareActivatedForKeepalive prepares an Activated Registration for future keepalive PrepareActivatedForKeepalive(*v1.Registration) (*v1.Registration, error) // Keepalive provides a heartbeat to SCC and validates the system's status. Keepalive(registrationObj *v1.Registration) error // PrepareKeepaliveSucceeded completes any necessary steps after successful keepalive PrepareKeepaliveSucceeded(*v1.Registration) (*v1.Registration, error) // Deregister initiates the system's deregistration from SCC. Deregister() error // ReconcileRegisterError prepares the Registration object for error reconciliation after RegisterSystem fails. ReconcileRegisterError(*v1.Registration, error, types.RegistrationPhase) *v1.Registration // ReconcileKeepaliveError prepares the Registration object for error reconciliation after Keepalive fails. ReconcileKeepaliveError(*v1.Registration, error) *v1.Registration // ReconcileActivateError prepares the Registration object for error reconciliation after Activate fails. ReconcileActivateError(*v1.Registration, error, types.ActivationPhase) *v1.Registration } type handler struct { ctx context.Context log *logrus.Entry options *types.RunOptions registrations registrationControllers.RegistrationController registrationCache registrationControllers.RegistrationCache secretRepo *secretrepo.SecretRepository settings *settings.SettingReader } // Register will setup the SCC registration CRDs controllers (and related secret controllers) // TODO: pull out secret stuff to their own controller func Register( ctx context.Context, options *types.RunOptions, registrations registrationControllers.RegistrationController, secretsRepo *secretrepo.SecretRepository, settings *settings.SettingReader, ) { controller := &handler{ log: log.NewControllerLogger("registration-controller"), ctx: ctx, options: options, registrations: registrations, registrationCache: registrations.Cache(), secretRepo: secretsRepo, settings: settings, } controller.initIndexers() controller.initResolvers(ctx) withinExpectedNamespaceCondition := func(name string, obj runtime.Object) (bool, error) { if !wranglerPolyfill.InExpectedNamespace(name, obj, controller.options.SystemNamespace()) { return false, nil } return true, nil } wranglerPolyfill.ScopedOnChange(ctx, controllerID+"-secrets", withinExpectedNamespaceCondition, secretsRepo.Controller, controller.OnSecretChange) wranglerPolyfill.ScopedOnRemove(ctx, controllerID+"-secrets-remove", withinExpectedNamespaceCondition, secretsRepo.Controller, controller.OnSecretRemove) // TODO: pull out registration controllers to register only when system is ready // TODO: also add a watcher to trigger enqueue on related resource changes withinOperatorScopeCondition := func(_ string, obj runtime.Object) (bool, error) { if obj == nil { return false, nil } metaObj, err := meta.Accessor(obj) if err != nil { return false, err } return helpers.ShouldManage(metaObj, controller.options.OperatorName), nil } wranglerPolyfill.ScopedOnChange(ctx, controllerID, withinOperatorScopeCondition, registrations, controller.OnRegistrationChange) wranglerPolyfill.ScopedOnRemove(ctx, controllerID+"-remove", withinOperatorScopeCondition, registrations, controller.OnRegistrationRemove) cfg := setupCfg() go controller.RunLifecycleManager(cfg, rancher.GetServerURL(ctx, settings)) } func (h *handler) prepareHandler(registrationObj *v1.Registration, rancherURL string) SCCHandler { ref := registrationObj.ToOwnerRef() nameSuffixHash := registrationObj.Labels[consts.LabelNameSuffix] defaultLabels := map[string]string{ consts.LabelSccHash: registrationObj.Labels[consts.LabelSccHash], consts.LabelNameSuffix: nameSuffixHash, consts.LabelSccManagedBy: controllerID, consts.LabelK8sManagedBy: initializer.OperatorName.Get(), } if registrationObj.Spec.Mode == v1.RegistrationModeOffline { offlineRequestSecretName := consts.OfflineRequestSecretName(nameSuffixHash) offlineCertSecretName := consts.OfflineCertificateSecretName(nameSuffixHash) return &sccOfflineMode{ rancherURL: rancherURL, rancherUUID: rancher.GetRancherInstallUUID(h.ctx, h.settings), log: h.log.WithField("regHandler", "offline"), options: h.options, registration: registrationObj, offlineSecrets: offline.New( h.options.SystemNamespace(), offlineRequestSecretName, offlineCertSecretName, ref, h.secretRepo, defaultLabels, ), } } credsSecretName := consts.SCCCredentialsSecretName(nameSuffixHash) return &sccOnlineMode{ rancherURL: rancherURL, log: h.log.WithField("regHandler", "online"), options: h.options, registration: registrationObj, sccCredentials: credentials.New( h.options.SystemNamespace(), credsSecretName, ref, h.secretRepo, defaultLabels, ), secretRepo: h.secretRepo, } } func (h *handler) OnSecretChange(_ string, incomingObj *corev1.Secret) (*corev1.Secret, error) { if incomingObj == nil || incomingObj.DeletionTimestamp != nil { return incomingObj, nil } if !h.isSCCEntrypointSecret(incomingObj) { return incomingObj, nil } // This only applies to the SCC Entrypoint secrets - currently only used for/by Rancher // This will adopt "unowned" secrets and ignore any that are owned by other operators if !helpers.ShouldManage(incomingObj, h.options.OperatorName) { // When the secret has no managedBy label, we should assume ownership I guess? if !helpers.HasManagedByLabel(incomingObj) { h.log.Debugf("taking ownership of the unowned entrypoint secret") prepared := incomingObj.DeepCopy() prepared = helpers.TakeOwnership(prepared, h.options.OperatorName) _, updateErr := h.secretRepo.RetryingPatchUpdate(incomingObj, prepared) if updateErr != nil { h.log.Errorf("failed to take ownership of secret %s/%s: %v", incomingObj.Namespace, incomingObj.Name, updateErr) return incomingObj, updateErr } h.log.Debugf("Secret %s/%s is now managed by %s", incomingObj.Namespace, incomingObj.Name, h.options.OperatorName) return incomingObj, nil } managedBy := helpers.GetManagedByValue(incomingObj) h.log.Debugf("Secret %s/%s is managed by %s not %s, skipping", incomingObj.Namespace, incomingObj.Name, managedBy, h.options.OperatorName) return incomingObj, nil } if _, saltOk := incomingObj.GetLabels()[consts.LabelObjectSalt]; !saltOk { return h.prepareSecretSalt(incomingObj) } incomingNameHash := incomingObj.GetLabels()[consts.LabelNameSuffix] incomingContentHash := incomingObj.GetLabels()[consts.LabelSccHash] params, err := extractRegistrationParamsFromSecret(incomingObj, h.options.OperatorName) if err != nil { return incomingObj, fmt.Errorf("failed to extract registration params from secret %s/%s: %w", incomingObj.Namespace, incomingObj.Name, err) } if incomingContentHash == "" { h.log.Debugf("incoming content hash empty, preparing secret %s/%s", incomingObj.Namespace, incomingObj.Name) // update secret with useful annotations & labels newSecret := incomingObj.DeepCopy() if newSecret.Annotations == nil { newSecret.Annotations = map[string]string{} } newSecret.Annotations[consts.LabelSccLastProcessed] = time.Now().Format(time.RFC3339) maps.Copy(newSecret.Labels, params.Labels()) _, updateErr := h.secretRepo.RetryingPatchUpdate(incomingObj, newSecret) if updateErr != nil { h.log.Error("error applying metadata updates to default SCC registration secret") return nil, updateErr } return incomingObj, nil } // If secret hash has changed make sure that we submit objects that correspond to that hash // are cleaned up // TODO: make it so that changes to the incoming Salt (which changes the nameID) are correctly handled // Note that change would affect both name and content hashes - however something seems to not. if incomingNameHash != params.nameID { h.log.Info("must cleanup existing registration managed by secret") if cleanUpErr := h.cleanupRegistrationByHash(hashCleanupRequest{ incomingNameHash, NameHash, }); cleanUpErr != nil { h.log.Errorf("failed to cleanup registrations for hash %s: %v", incomingNameHash, cleanUpErr) return incomingObj, cleanUpErr } } // TODO: rework stuff around this as this shouldn't be necessary if incomingContentHash != params.contentHash { h.log.Info("must cleanup existing registration managed by secret") if cleanUpErr := h.cleanupRelatedSecretsByHash(incomingContentHash); cleanUpErr != nil { h.log.Errorf("failed to cleanup registrations for hash %s: %v", incomingNameHash, cleanUpErr) return incomingObj, cleanUpErr } } h.log.Info("create or update registration managed by secret") // update secret with useful annotations & labels newSecret := incomingObj.DeepCopy() if newSecret.Annotations == nil { newSecret.Annotations = map[string]string{} } newSecret.Annotations[consts.LabelSccLastProcessed] = time.Now().Format(time.RFC3339) labels := incomingObj.Labels maps.Copy(labels, params.Labels()) newSecret.Labels = labels if _, err := h.secretRepo.RetryingPatchUpdate(incomingObj, newSecret); err != nil { return incomingObj, err } if params.regType == v1.RegistrationModeOffline && params.hasOfflineCertData { offlineCertSecret, err := h.offlineCertFromSecretEntrypoint(params) if err != nil { return incomingObj, err } if _, err := h.secretRepo.CreateOrUpdateSecret(offlineCertSecret); err != nil { return incomingObj, err } } if params.regType == v1.RegistrationModeOnline { regCodeSecret, err := h.regCodeFromSecretEntrypoint(params) if err != nil { return incomingObj, err } if _, err := h.secretRepo.CreateOrUpdateSecret(regCodeSecret); err != nil { return incomingObj, err } } // construct associated registration CRs registration, err := h.registrationFromSecretEntrypoint(params) if err != nil { return incomingObj, fmt.Errorf("failed to create registration from secret %s/%s: %w", incomingObj.Namespace, incomingObj.Name, err) } if createOrUpdateErr := h.createOrUpdateRegistration(registration); createOrUpdateErr != nil { h.log.Errorf("failed to create or update registration %s: %v", registration.Name, createOrUpdateErr) return incomingObj, fmt.Errorf("failed to create or update registration %s: %w", registration.Name, createOrUpdateErr) } return incomingObj, nil } func (h *handler) cleanupRegistrationByHash(cleanupRequest hashCleanupRequest) error { var regs []*v1.Registration var err error if cleanupRequest.hashType == ContentHash { regs, err = h.registrationCache.GetByIndex(IndexRegistrationsBySccHash, cleanupRequest.hash) } else { regs, err = h.registrationCache.GetByIndex(IndexRegistrationsByNameHash, cleanupRequest.hash) } if err != nil { return err } h.log.Infof("found %d matching registrations to clean up the %s hash", len(regs), cleanupRequest.hashType) for _, reg := range regs { if !slices.Contains(reg.Finalizers, consts.FinalizerSccRegistration) { continue } if retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { var remainingFin []string for _, finalizer := range reg.Finalizers { if finalizer != consts.FinalizerSccRegistration { remainingFin = append(remainingFin, finalizer) } } reg.Finalizers = remainingFin _, updateErr := h.registrations.Update(reg) return updateErr }); retryErr != nil { return retryErr } deleteErr := h.registrations.Delete(reg.Name, &metav1.DeleteOptions{}) if apierrors.IsNotFound(deleteErr) { h.log.Debugf("Registration %s already deleted", reg.Name) continue } if deleteErr != nil { return fmt.Errorf("failed to delete registration %s: %w", reg.Name, deleteErr) } } return nil } func (h *handler) cleanupRelatedSecretsByHash(contentHash string) error { secrets, err := h.secretRepo.GetBySccContentHash(contentHash) h.log.Infof("found %d matching related secrets to clean up; content hash of %s", len(secrets), contentHash) if err != nil { return err } // It should never be in there, but just in case don't act on the entrypoint secrets = slices.Collect(func(yield func(secret *corev1.Secret) bool) { for _, secret := range secrets { if secret.Name != consts.ResourceSCCEntrypointSecretName && !strings.HasPrefix(secret.Name, consts.OfflineRequestSecretNamePrefix) { if !yield(secret) { return } } } }) for _, secret := range secrets { if shared.SecretHasCredentialsFinalizer(secret) || shared.SecretHasRegCodeFinalizer(secret) { var updateErr error secretUpdated := secret.DeepCopy() secretUpdated = shared.SecretRemoveCredentialsFinalizer(secretUpdated) secretUpdated = shared.SecretRemoveRegCodeFinalizer(secretUpdated) _, updateErr = h.secretRepo.RetryingPatchUpdate(secret, secretUpdated) if updateErr != nil { h.log.Errorf("failed to update secret %s/%s: %v", secret.Namespace, secret.Name, updateErr) return updateErr } } deleteErr := h.secretRepo.Controller.Delete(secret.Namespace, secret.Name, &metav1.DeleteOptions{}) if apierrors.IsNotFound(deleteErr) { h.log.Debugf("Related Secret %s/%s already deleted", secret.Namespace, secret.Name) continue } if deleteErr != nil { return fmt.Errorf("failed to delete secret %s/%s: %w", secret.Namespace, secret.Name, deleteErr) } } return nil } func (h *handler) OnSecretRemove(_ string, incomingObj *corev1.Secret) (*corev1.Secret, error) { if incomingObj == nil { return nil, nil } if incomingObj.Namespace != h.options.SystemNamespace() { h.log.Debugf("Secret %s/%s is not in SCC system namespace %s, skipping cleanup", incomingObj.Namespace, incomingObj.Name, h.options.SystemNamespace()) return incomingObj, nil } if !helpers.ShouldManage(incomingObj, h.options.OperatorName) { h.log.Debugf("Secret %s/%s is not managed by %s, skipping", incomingObj.Namespace, incomingObj.Name, h.options.OperatorName) return incomingObj, nil } if h.isSCCEntrypointSecret(incomingObj) { hash, ok := incomingObj.Labels[consts.LabelNameSuffix] if !ok { return incomingObj, nil } // TODO: (alex) needs some thought about how we actually map entrypoint secret cleanup // here based on the control flow changes in OnChange if err := h.cleanupRegistrationByHash(hashCleanupRequest{ hash, NameHash, }); err != nil { h.log.Errorf("failed to cleanup registrations for hash %s: %v", hash, err) return nil, err } contentHash, ok := incomingObj.Labels[consts.LabelSccHash] if !ok { return incomingObj, nil } if cleanUpErr := h.cleanupRelatedSecretsByHash(contentHash); cleanUpErr != nil { h.log.Errorf("failed to cleanup registrations for hash %s: %v", hash, cleanUpErr) return incomingObj, cleanUpErr } return incomingObj, nil } if shared.SecretHasCredentialsFinalizer(incomingObj) || shared.SecretHasRegCodeFinalizer(incomingObj) { refs := incomingObj.GetOwnerReferences() danglingRefs := 0 for _, ref := range refs { if ref.APIVersion == v1.SchemeGroupVersion.String() && ref.Kind == "Registration" { reg, err := h.registrations.Get(ref.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { continue } if reg.DeletionTimestamp == nil && reg.Status.ActivationStatus.Activated { danglingRefs++ } else { // TODO(alex): verify this logic when you are back // When reg is marked to delete too - we may need to help clean it up regFinalizers := reg.GetFinalizers() if len(regFinalizers) > 0 && slices.Contains(regFinalizers, consts.FinalizerSccRegistration) { regUpdate := reg.DeepCopy() removeIndex := slices.Index(regFinalizers, consts.FinalizerSccRegistration) regUpdate.Finalizers = append(reg.Finalizers[:removeIndex], reg.Finalizers[removeIndex+1:]...) _, err = h.patchUpdateRegistration(reg, regUpdate) if err != nil { h.log.Errorf("failed to patch registration %s/%s: %v", reg.Namespace, reg.Name, err) } } } } } if danglingRefs > 0 { h.log.Errorf("cannot remove SCC finalizer from secret %s/%s, dangling references to Registration found", incomingObj.Namespace, incomingObj.Name) return nil, fmt.Errorf("cannot remove SCC finalizer from secret %s/%s, dangling references to Registration found", incomingObj.Namespace, incomingObj.Name) } newSecret := incomingObj.DeepCopy() if shared.SecretHasCredentialsFinalizer(newSecret) { newSecret = shared.SecretRemoveCredentialsFinalizer(newSecret) } if shared.SecretHasRegCodeFinalizer(newSecret) { newSecret = shared.SecretRemoveRegCodeFinalizer(newSecret) } logrus.Info("Removing finalizer from secret", newSecret.Name, "in namespace", newSecret.Namespace) if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { _, err := h.secretRepo.PatchUpdate(incomingObj, newSecret) return err }); err != nil { h.log.Errorf("failed to remove SCC finalizer from secret %s/%s: %v", incomingObj.Namespace, incomingObj.Name, err) return nil, fmt.Errorf("failed to remove SCC finalizer from secret %s/%s: %w", incomingObj.Namespace, incomingObj.Name, err) } } return incomingObj, nil } func (h *handler) OnRegistrationChange(_ string, registrationObj *v1.Registration) (*v1.Registration, error) { activiateMu.Lock() defer activiateMu.Unlock() if registrationObj == nil || registrationObj.DeletionTimestamp != nil { return nil, nil } rancherURL := rancher.GetServerURL(h.ctx, h.settings) if rancherURL == "" { h.log.Info("Server URL not set") return registrationObj, errors.New("no server url found in the system info") } // TODO: if there are more than one operators per cluster (for other products) check managedBy status registrationHandler := h.prepareHandler(registrationObj, rancherURL) if registrationObj.Spec.Mode == v1.RegistrationModeOffline { if v1.ResourceConditionFailure.IsTrue(registrationObj) && v1.RegistrationConditionOfflineCertificateReady.IsFalse(registrationObj) && v1.RegistrationConditionOfflineCertificateReady.GetMessage(registrationObj) != InitialOfflineCertificateReadyMessage && registrationObj.Spec.OfflineRegistrationCertificateSecretRef == nil { h.log.Info("registration is failed but user removed certificate. Resetting registration status back to ReadyForActivation") resetUpdateErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { reset := registrationObj.DeepCopy() reset, resetErr := registrationHandler.ResetToRegisteredForActivation(reset) if resetErr != nil { return resetErr } _, updateErr := h.registrations.UpdateStatus(reset) return updateErr }) if resetUpdateErr != nil { return registrationObj, resetUpdateErr } return registrationObj, nil } } if shared.RegistrationIsFailed(registrationObj) { failedCondition := registrationObj.Status.CurrentCondition if failedCondition != nil { h.log.Errorf("registration `%s` has the Failure status condition from: %v", registrationObj.Name, failedCondition) } else { h.log.Errorf("registration `%s` has the Failure status condition active", registrationObj.Name) } h.log.Warnf("reviewing the registration `%s` for other errors is advised before retrying", registrationObj.Name) errorFixHint := fmt.Sprintf("delete this registration `%s` and then create a new one to try again.", registrationObj.Name) if shared.RegistrationHasManagedFinalizer(registrationObj) { errorFixHint = fmt.Sprintf("delete the entrypoint secret `%s/%s`, give it time to clean up, and then create a new one to try again.", h.options.SystemNamespace(), consts.ResourceSCCEntrypointSecretName) } h.log.Warn("after resolving the issue(s), " + errorFixHint) return registrationObj, nil } // Skip keepalive for anything activated within the last 20 hours if !registrationHandler.NeedsRegistration(registrationObj) && !registrationHandler.NeedsActivation(registrationObj) && registrationObj.Spec.SyncNow == nil { if !registrationObj.Status.ActivationStatus.LastValidatedTS.IsZero() && registrationObj.Status.ActivationStatus.LastValidatedTS.Time.After(minResyncInterval()) { return registrationObj, nil } } // Fetch Rancher metrics for SCC systemMetrics, metricsErr := h.secretRepo.FetchMetricsSecret() if metricsErr != nil { wrappedErr := fmt.Errorf("encountered additional error when preparing SCC handler: %v", metricsErr) h.log.Error(wrappedErr) return registrationObj, wrappedErr } // TODO: parse out the secret data registrationHandler.SetRancherMetrics(systemMetrics) // Only on the first time an object passes through here should it need to be registered // The logical default condition should always be to try activation, unless we know it's not registered. if registrationHandler.NeedsRegistration(registrationObj) { if !registrationObj.HasCondition(v1.ResourceConditionProgressing) || v1.ResourceConditionProgressing.IsFalse(registrationObj) { progressingObj := registrationObj.DeepCopy() // Set object to progressing progressingUpdateErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { var err error v1.ResourceConditionProgressing.True(progressingObj) // Set ResourceConditionProgressing as the CurrentCondition since we're starting the registration process progressingObj.SetCurrentCondition(v1.ResourceConditionProgressing) progressingObj, err = h.registrations.UpdateStatus(progressingObj) return err }) if progressingUpdateErr != nil { return registrationObj, progressingUpdateErr } return registrationObj, nil } // Start of initial registration/announce of cluster regForAnnounce := registrationObj.DeepCopy() preparedForRegister, prepareErr := registrationHandler.PrepareForRegister(regForAnnounce) if prepareErr != nil { err := h.reconcileRegistration(registrationHandler, preparedForRegister, prepareErr, types.RegistrationPrepare) return registrationObj, err } var updateErr error if regForAnnounce, updateErr = h.registrations.UpdateStatus(preparedForRegister); updateErr != nil { return registrationObj, updateErr } announcedSystemID, registerErr := registrationHandler.Register(regForAnnounce) if registerErr != nil { err := h.reconcileRegistration(registrationHandler, preparedForRegister, registerErr, types.RegistrationMain) return registrationObj, err } setSystemID := false switch announcedSystemID { case suseconnect.OfflineRegistrationSystemID: h.log.Debugf("SCC system ID cannot be known for offline until activation") case suseconnect.KeepAliveRegistrationSystemID: h.log.Debugf("register system handled via keepalive") announcedSystemID = suseconnect.RegistrationSystemID(*registrationObj.Status.SCCSystemID) default: h.log.Debugf("Annoucned System ID: %v", announcedSystemID) setSystemID = true } var prepareError error // Prepare the Registration for Activation phase if setSystemID { regForAnnounce.Status.SCCSystemID = announcedSystemID.Ptr() } regForAnnounce, prepareError = registrationHandler.PrepareRegisteredForActivation(regForAnnounce) if prepareError != nil { err := h.reconcileRegistration(registrationHandler, preparedForRegister, prepareError, types.RegistrationForActivation) return registrationObj, err } regForAnnounce.Status.RegistrationProcessedTS = &metav1.Time{ Time: time.Now(), } _, registerUpdateErr := h.registrations.UpdateStatus(regForAnnounce) if registerUpdateErr != nil { return registrationObj, registerUpdateErr } return registrationObj, nil } if registrationHandler.NeedsActivation(registrationObj) { if !registrationHandler.ReadyForActivation(registrationObj) { h.log.Debugf("registration needs to be activated, but not yet ready; %v", registrationObj) return registrationObj, nil } activationErr := registrationHandler.Activate(registrationObj) // reconcile error state - must be able to handle Auth errors (or other SCC sourced errors) if activationErr != nil { err := h.reconcileActivation(registrationHandler, registrationObj, activationErr, types.ActivationMain) return registrationObj, err } activatedUpdateErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { var retryErr, updateErr error registrationObj, retryErr = h.registrations.Get(registrationObj.Name, metav1.GetOptions{}) if retryErr != nil { return retryErr } activated := registrationObj.DeepCopy() activated = shared.PrepareSuccessfulActivation(activated) prepared, err := registrationHandler.PrepareActivatedForKeepalive(activated) if err != nil { err := h.reconcileActivation(registrationHandler, registrationObj, activationErr, types.ActivationPrepForKeepalive) return err } _, updateErr = h.registrations.UpdateStatus(prepared) return updateErr }) if activatedUpdateErr != nil { return registrationObj, activatedUpdateErr } return registrationObj, nil } // Handle what to do when CheckNow is used... if shared.RegistrationNeedsSyncNow(registrationObj) { if registrationObj.Spec.Mode == v1.RegistrationModeOffline { updated := registrationObj.DeepCopy() updated.Spec = *registrationObj.Spec.WithoutSyncNow() offlineHandler := registrationHandler.(*sccOfflineMode) refreshErr := offlineHandler.RefreshOfflineRequestSecret() _, updateErr := h.registrations.Update(updated) if updateErr != nil || refreshErr != nil { return registrationObj, errors.Join(refreshErr, updateErr) } return registrationObj, nil } // Todo: online/offline handler interface should have a SyncNow call to get rid of the if here updated := registrationObj.DeepCopy() updated.Spec = *registrationObj.Spec.WithoutSyncNow() updated.Status.ActivationStatus.Activated = false updated.Status.ActivationStatus.LastValidatedTS = &metav1.Time{} v1.ResourceConditionProgressing.True(updated) v1.ResourceConditionReady.False(updated) v1.ResourceConditionDone.False(updated) v1.RegistrationConditionActivated.False(updated) // Set ResourceConditionProgressing as the CurrentCondition since we're resetting the registration process updated.SetCurrentCondition(v1.ResourceConditionProgressing) var err error updated, err = h.registrations.UpdateStatus(updated) if err != nil { // TODO handle this error better via ReconcileSyncNow return registrationObj, err } updated.Spec = *registrationObj.Spec.WithoutSyncNow() updated, err = h.registrations.Update(updated) return registrationObj, err } keepaliveErr := registrationHandler.Keepalive(registrationObj) if keepaliveErr != nil { reconcileErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { curReg, getErr := h.registrations.Get(registrationObj.Name, metav1.GetOptions{}) if getErr != nil { return getErr } prepareObj := curReg.DeepCopy() prepareObj = registrationHandler.ReconcileKeepaliveError(prepareObj, keepaliveErr) _, reconcileUpdateErr := h.registrations.Update(prepareObj) return reconcileUpdateErr }) err := fmt.Errorf("keepalive failed: %w", keepaliveErr) if reconcileErr != nil { err = fmt.Errorf("keepalive failed with additional errors: %w, %w", keepaliveErr, reconcileErr) } return registrationObj, err } keepaliveUpdateErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { var retryErr, updateErr error registrationObj, retryErr = h.registrations.Get(registrationObj.Name, metav1.GetOptions{}) if retryErr != nil { return retryErr } keepalive := registrationObj.DeepCopy() keepalive = shared.PrepareSuccessfulActivation(keepalive) v1.RegistrationConditionKeepalive.True(keepalive) prepared, err := registrationHandler.PrepareKeepaliveSucceeded(keepalive) if err != nil { return err } _, updateErr = h.registrations.UpdateStatus(prepared) return updateErr }) if keepaliveUpdateErr != nil { return registrationObj, keepaliveUpdateErr } return registrationObj, nil } func (h *handler) OnRegistrationRemove(name string, registrationObj *v1.Registration) (*v1.Registration, error) { if registrationObj == nil { return nil, nil } rancherURL := rancher.GetServerURL(h.ctx, h.settings) if rancherURL == "" { h.log.Info("Server URL not set") return registrationObj, errors.New("no server url found in the system info") } regHandler := h.prepareHandler(registrationObj, rancherURL) deRegErr := regHandler.Deregister() if deRegErr != nil { h.log.Warn(deRegErr) } err := h.registrations.Delete(name, &metav1.DeleteOptions{}) if err != nil { return registrationObj, err } return nil, nil }