Prevent runnerset pod unregistration until it gets runner ID
This eliminates the race condition that results in the runner terminated prematurely when RunnerSet triggered unregistration of StatefulSet that added just a few seconds ago.
This commit is contained in:
		
							parent
							
								
									15b402bb32
								
							
						
					
					
						commit
						a3072c110d
					
				|  | @ -602,7 +602,7 @@ func (r *RunnerReconciler) processRunnerCreation(ctx context.Context, runner v1a | |||
| // - (false, err) when it postponed unregistration due to the runner being busy, or it tried to unregister the runner but failed due to
 | ||||
| //   an error returned by GitHub API.
 | ||||
| func (r *RunnerReconciler) unregisterRunner(ctx context.Context, enterprise, org, repo, name string) (bool, error) { | ||||
| 	return unregisterRunner(ctx, r.GitHubClient, enterprise, org, repo, name) | ||||
| 	return unregisterRunner(ctx, r.GitHubClient, enterprise, org, repo, name, nil) | ||||
| } | ||||
| 
 | ||||
| func (r *RunnerReconciler) updateRegistrationToken(ctx context.Context, runner v1alpha1.Runner) (bool, error) { | ||||
|  |  | |||
|  | @ -4,19 +4,33 @@ import ( | |||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/actions-runner-controller/actions-runner-controller/github" | ||||
| 	"github.com/go-logr/logr" | ||||
| 	gogithub "github.com/google/go-github/v39/github" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	ctrl "sigs.k8s.io/controller-runtime" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// unregistrationCompleteTimestamp is the annotation that is added onto the pod once the previously started unregistration process has been completed.
 | ||||
| 	unregistrationCompleteTimestamp = "unregistration-complete-timestamp" | ||||
| 	unregistrationStartTimestamp    = "unregistration-start-timestamp" | ||||
| 
 | ||||
| 	// unregistarionStartTimestamp is the annotation that contains the time that the requested unregistration process has been started
 | ||||
| 	unregistrationStartTimestamp = "unregistration-start-timestamp" | ||||
| 
 | ||||
| 	// unregistrationRequestTimestamp is the annotation that contains the time that the unregistration has been requested.
 | ||||
| 	// This doesn't immediately start the unregistration. Instead, ARC will first check if the runner has already been registered.
 | ||||
| 	// If not, ARC will hold on until the registration to complete first, and only after that it starts the unregistration process.
 | ||||
| 	// This is crucial to avoid a race between ARC marking the runner pod for deletion while the actions-runner registers itself to GitHub, leaving the assigned job
 | ||||
| 	// hang like forever.
 | ||||
| 	unregistrationRequestTimestamp = "unregistration-request-timestamp" | ||||
| 
 | ||||
| 	annotationKeyRunnerID = "runner-id" | ||||
| 
 | ||||
| 	// DefaultUnregistrationTimeout is the duration until ARC gives up retrying the combo of ListRunners API (to detect the runner ID by name)
 | ||||
| 	// and RemoveRunner API (to actually unregister the runner) calls.
 | ||||
|  | @ -40,48 +54,61 @@ const ( | |||
| // When it wants to be retried later, the function returns a non-nil *ctrl.Result as the second return value, may or may not populating the error in the second return value.
 | ||||
| // The caller is expected to return the returned ctrl.Result and error to postpone the current reconcilation loop and trigger a scheduled retry.
 | ||||
| func tickRunnerGracefulStop(ctx context.Context, unregistrationTimeout time.Duration, retryDelay time.Duration, log logr.Logger, ghClient *github.Client, c client.Client, enterprise, organization, repository, runner string, pod *corev1.Pod) (*corev1.Pod, *ctrl.Result, error) { | ||||
| 	if pod != nil { | ||||
| 		if _, ok := getAnnotation(pod, unregistrationStartTimestamp); !ok { | ||||
| 			updated := pod.DeepCopy() | ||||
| 			setAnnotation(updated, unregistrationStartTimestamp, time.Now().Format(time.RFC3339)) | ||||
| 			if err := c.Patch(ctx, updated, client.MergeFrom(pod)); err != nil { | ||||
| 				log.Error(err, fmt.Sprintf("Failed to patch pod to have %s annotation", unregistrationStartTimestamp)) | ||||
| 				return nil, &ctrl.Result{}, err | ||||
| 			} | ||||
| 			pod = updated | ||||
| 
 | ||||
| 			log.Info("Runner has started unregistration") | ||||
| 		} else { | ||||
| 			log.Info("Runner has already started unregistration") | ||||
| 		} | ||||
| 	pod, err := annotatePodOnce(ctx, c, log, pod, unregistrationStartTimestamp, time.Now().Format(time.RFC3339)) | ||||
| 	if err != nil { | ||||
| 		return nil, &ctrl.Result{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	if res, err := ensureRunnerUnregistration(ctx, unregistrationTimeout, retryDelay, log, ghClient, enterprise, organization, repository, runner, pod); res != nil { | ||||
| 		return nil, res, err | ||||
| 	} | ||||
| 
 | ||||
| 	if pod != nil { | ||||
| 		if _, ok := getAnnotation(pod, unregistrationCompleteTimestamp); !ok { | ||||
| 			updated := pod.DeepCopy() | ||||
| 			setAnnotation(updated, unregistrationCompleteTimestamp, time.Now().Format(time.RFC3339)) | ||||
| 			if err := c.Patch(ctx, updated, client.MergeFrom(pod)); err != nil { | ||||
| 				log.Error(err, fmt.Sprintf("Failed to patch pod to have %s annotation", unregistrationCompleteTimestamp)) | ||||
| 				return nil, &ctrl.Result{}, err | ||||
| 			} | ||||
| 			pod = updated | ||||
| 
 | ||||
| 			log.Info("Runner has completed unregistration") | ||||
| 		} else { | ||||
| 			log.Info("Runner has already completed unregistration") | ||||
| 		} | ||||
| 	pod, err = annotatePodOnce(ctx, c, log, pod, unregistrationCompleteTimestamp, time.Now().Format(time.RFC3339)) | ||||
| 	if err != nil { | ||||
| 		return nil, &ctrl.Result{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return pod, nil, nil | ||||
| } | ||||
| 
 | ||||
| // annotatePodOnce annotates the pod if it wasn't.
 | ||||
| // Returns the provided pod as-is if it was already annotated.
 | ||||
| // Returns the updated pod if the pod was missing the annotation and the update to add the annotation succeeded.
 | ||||
| func annotatePodOnce(ctx context.Context, c client.Client, log logr.Logger, pod *corev1.Pod, k, v string) (*corev1.Pod, error) { | ||||
| 	if pod == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if _, ok := getAnnotation(&pod.ObjectMeta, k); ok { | ||||
| 		return pod, nil | ||||
| 	} | ||||
| 
 | ||||
| 	updated := pod.DeepCopy() | ||||
| 	setAnnotation(&updated.ObjectMeta, k, v) | ||||
| 	if err := c.Patch(ctx, updated, client.MergeFrom(pod)); err != nil { | ||||
| 		log.Error(err, fmt.Sprintf("Failed to patch pod to have %s annotation", k)) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	log.V(2).Info("Annotated pod", "key", k, "value", v) | ||||
| 
 | ||||
| 	return updated, nil | ||||
| } | ||||
| 
 | ||||
| // If the first return value is nil, it's safe to delete the runner pod.
 | ||||
| func ensureRunnerUnregistration(ctx context.Context, unregistrationTimeout time.Duration, retryDelay time.Duration, log logr.Logger, ghClient *github.Client, enterprise, organization, repository, runner string, pod *corev1.Pod) (*ctrl.Result, error) { | ||||
| 	ok, err := unregisterRunner(ctx, ghClient, enterprise, organization, repository, runner) | ||||
| 	var runnerID *int64 | ||||
| 
 | ||||
| 	if id, ok := getAnnotation(&pod.ObjectMeta, annotationKeyRunnerID); ok { | ||||
| 		v, err := strconv.ParseInt(id, 10, 64) | ||||
| 		if err != nil { | ||||
| 			return &ctrl.Result{}, err | ||||
| 		} | ||||
| 
 | ||||
| 		runnerID = &v | ||||
| 	} | ||||
| 
 | ||||
| 	ok, err := unregisterRunner(ctx, ghClient, enterprise, organization, repository, runner, runnerID) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, &gogithub.RateLimitError{}) { | ||||
| 			// We log the underlying error when we failed calling GitHub API to list or unregisters,
 | ||||
|  | @ -125,7 +152,7 @@ func ensureRunnerUnregistration(ctx context.Context, unregistrationTimeout time. | |||
| 
 | ||||
| 		return &ctrl.Result{}, err | ||||
| 	} else if ok { | ||||
| 		log.Info("Runner has just been unregistered. Removing the runner pod.") | ||||
| 		log.Info("Runner has just been unregistered.") | ||||
| 	} else if pod == nil { | ||||
| 		// `r.unregisterRunner()` will returns `false, nil` if the runner is not found on GitHub.
 | ||||
| 		// However, that doesn't always mean the pod can be safely removed.
 | ||||
|  | @ -174,22 +201,47 @@ func ensureRunnerUnregistration(ctx context.Context, unregistrationTimeout time. | |||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| func getAnnotation(pod *corev1.Pod, key string) (string, bool) { | ||||
| 	if pod.Annotations == nil { | ||||
| func ensureRunnerPodRegistered(ctx context.Context, log logr.Logger, ghClient *github.Client, c client.Client, enterprise, organization, repository, runner string, pod *corev1.Pod) (*corev1.Pod, *ctrl.Result, error) { | ||||
| 	_, hasRunnerID := getAnnotation(&pod.ObjectMeta, annotationKeyRunnerID) | ||||
| 	if runnerPodOrContainerIsStopped(pod) || hasRunnerID { | ||||
| 		return pod, nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	r, err := getRunner(ctx, ghClient, enterprise, organization, repository, runner) | ||||
| 	if err != nil { | ||||
| 		return nil, &ctrl.Result{RequeueAfter: 10 * time.Second}, err | ||||
| 	} | ||||
| 
 | ||||
| 	if r == nil || r.ID == nil { | ||||
| 		return nil, &ctrl.Result{RequeueAfter: 10 * time.Second}, err | ||||
| 	} | ||||
| 
 | ||||
| 	id := *r.ID | ||||
| 
 | ||||
| 	updated, err := annotatePodOnce(ctx, c, log, pod, annotationKeyRunnerID, fmt.Sprintf("%d", id)) | ||||
| 	if err != nil { | ||||
| 		return nil, &ctrl.Result{RequeueAfter: 10 * time.Second}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return updated, nil, nil | ||||
| } | ||||
| 
 | ||||
| func getAnnotation(meta *metav1.ObjectMeta, key string) (string, bool) { | ||||
| 	if meta.Annotations == nil { | ||||
| 		return "", false | ||||
| 	} | ||||
| 
 | ||||
| 	v, ok := pod.Annotations[key] | ||||
| 	v, ok := meta.Annotations[key] | ||||
| 
 | ||||
| 	return v, ok | ||||
| } | ||||
| 
 | ||||
| func setAnnotation(pod *corev1.Pod, key, value string) { | ||||
| 	if pod.Annotations == nil { | ||||
| 		pod.Annotations = map[string]string{} | ||||
| func setAnnotation(meta *metav1.ObjectMeta, key, value string) { | ||||
| 	if meta.Annotations == nil { | ||||
| 		meta.Annotations = map[string]string{} | ||||
| 	} | ||||
| 
 | ||||
| 	pod.Annotations[key] = value | ||||
| 	meta.Annotations[key] = value | ||||
| } | ||||
| 
 | ||||
| // unregisterRunner unregisters the runner from GitHub Actions by name.
 | ||||
|  | @ -227,17 +279,19 @@ func setAnnotation(pod *corev1.Pod, key, value string) { | |||
| // There isn't a single right grace period that works for everyone.
 | ||||
| // The longer the grace period is, the earlier a cluster resource shortage can occur due to throttoled runner pod deletions,
 | ||||
| // while the shorter the grace period is, the more likely you may encounter the race issue.
 | ||||
| func unregisterRunner(ctx context.Context, client *github.Client, enterprise, org, repo, name string) (bool, error) { | ||||
| 	runner, err := getRunner(ctx, client, enterprise, org, repo, name) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| func unregisterRunner(ctx context.Context, client *github.Client, enterprise, org, repo, name string, id *int64) (bool, error) { | ||||
| 	if id == nil { | ||||
| 		runner, err := getRunner(ctx, client, enterprise, org, repo, name) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 
 | ||||
| 	if runner == nil || runner.ID == nil { | ||||
| 		return false, nil | ||||
| 	} | ||||
| 		if runner == nil || runner.ID == nil { | ||||
| 			return false, nil | ||||
| 		} | ||||
| 
 | ||||
| 	id := *runner.ID | ||||
| 		id = runner.ID | ||||
| 	} | ||||
| 
 | ||||
| 	// For the record, historically ARC did not try to call RemoveRunner on a busy runner, but it's no longer true.
 | ||||
| 	// The reason ARC did so was to let a runner running a job to not stop prematurely.
 | ||||
|  | @ -259,7 +313,7 @@ func unregisterRunner(ctx context.Context, client *github.Client, enterprise, or | |||
| 	//   change from 60 seconds.
 | ||||
| 	//
 | ||||
| 	// TODO: Probably we can just remove the runner by ID without seeing if the runner is busy, by treating it as busy when a remove-runner call failed with 422?
 | ||||
| 	if err := client.RemoveRunner(ctx, enterprise, org, repo, id); err != nil { | ||||
| 	if err := client.RemoveRunner(ctx, enterprise, org, repo, *id); err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -102,9 +102,13 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( | |||
| 				return ctrl.Result{}, err | ||||
| 			} | ||||
| 
 | ||||
| 			log.V(2).Info("Added finalizer") | ||||
| 
 | ||||
| 			return ctrl.Result{}, nil | ||||
| 		} | ||||
| 	} else { | ||||
| 		log.V(2).Info("Seen deletion-timestamp is already set") | ||||
| 
 | ||||
| 		finalizers, removed := removeFinalizer(runnerPod.ObjectMeta.Finalizers, runnerPodFinalizerName) | ||||
| 
 | ||||
| 		if removed { | ||||
|  | @ -122,7 +126,9 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( | |||
| 				return ctrl.Result{}, err | ||||
| 			} | ||||
| 
 | ||||
| 			log.Info("Removed runner from GitHub", "repository", repo, "organization", org) | ||||
| 			log.V(2).Info("Removed finalizer") | ||||
| 
 | ||||
| 			return ctrl.Result{}, nil | ||||
| 		} | ||||
| 
 | ||||
| 		deletionTimeout := 1 * time.Minute | ||||
|  | @ -160,6 +166,35 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( | |||
| 		return ctrl.Result{}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	po, res, err := ensureRunnerPodRegistered(ctx, log, r.GitHubClient, r.Client, enterprise, org, repo, runnerPod.Name, &runnerPod) | ||||
| 	if res != nil { | ||||
| 		return *res, err | ||||
| 	} | ||||
| 
 | ||||
| 	runnerPod = *po | ||||
| 
 | ||||
| 	if _, unregistrationRequested := getAnnotation(&runnerPod.ObjectMeta, unregistrationRequestTimestamp); unregistrationRequested { | ||||
| 		log.V(2).Info("Progressing unregistration because unregistration-request timestamp is set") | ||||
| 
 | ||||
| 		// At this point we're sure that DeletionTimestamp is not set yet, but the unregistration process is triggered by an upstream controller like runnerset-controller.
 | ||||
| 		//
 | ||||
| 		// In a standard scenario, ARC starts the unregistration process before marking the pod for deletion at all,
 | ||||
| 		// so that it isn't subject to terminationGracePeriod and can safely take hours to finish it's work.
 | ||||
| 		_, res, err := tickRunnerGracefulStop(ctx, r.unregistrationTimeout(), r.unregistrationRetryDelay(), log, r.GitHubClient, r.Client, enterprise, org, repo, runnerPod.Name, &runnerPod) | ||||
| 		if res != nil { | ||||
| 			return *res, err | ||||
| 		} | ||||
| 
 | ||||
| 		// At this point we are sure that the runner has successfully unregistered, hence is safe to be deleted.
 | ||||
| 		// But we don't delete the pod here. Instead, let the upstream controller/parent object to delete this pod as
 | ||||
| 		// a part of a cascade deletion.
 | ||||
| 		// This is to avoid a parent object, like statefulset, to recreate the deleted pod.
 | ||||
| 		// If the pod was recreated, it will start a registration process and that may race with the statefulset deleting the pod.
 | ||||
| 		log.V(2).Info("Unregistration seems complete") | ||||
| 
 | ||||
| 		return ctrl.Result{}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// If pod has ended up succeeded we need to restart it
 | ||||
| 	// Happens e.g. when dind is in runner and run completes
 | ||||
| 	stopped := runnerPod.Status.Phase == corev1.PodSucceeded | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ package controllers | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"sort" | ||||
| 	"time" | ||||
|  | @ -154,10 +155,53 @@ func (r *RunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( | |||
| 			return ctrl.Result{}, err | ||||
| 		} | ||||
| 
 | ||||
| 		// Statefulset termination process 4/4: Let Kubernetes cascade-delete the statefulset and the pods.
 | ||||
| 		if !res.statefulset.DeletionTimestamp.IsZero() { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// Statefulset termination process 3/4: Set the deletionTimestamp to let Kubernetes start a cascade deletion of the statefulset and the pods.
 | ||||
| 		if _, ok := getAnnotation(&res.statefulset.ObjectMeta, unregistrationCompleteTimestamp); ok { | ||||
| 			if err := r.Client.Delete(ctx, res.statefulset); err != nil { | ||||
| 				log.Error(err, "Failed to delete statefulset") | ||||
| 				return ctrl.Result{}, err | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// Statefulset termination process 2/4: Set unregistrationCompleteTimestamp only if all the pods managed by the statefulset
 | ||||
| 		// have either unregistered or being deleted.
 | ||||
| 		if _, ok := getAnnotation(&res.statefulset.ObjectMeta, unregistrationRequestTimestamp); ok { | ||||
| 			var deletionSafe int | ||||
| 			for _, po := range res.pods { | ||||
| 				if _, ok := getAnnotation(&po.ObjectMeta, unregistrationCompleteTimestamp); ok { | ||||
| 					deletionSafe++ | ||||
| 				} else if !po.DeletionTimestamp.IsZero() { | ||||
| 					deletionSafe++ | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			log.V(2).Info("Marking statefulset for unregistration completion", "deletionSafe", deletionSafe, "total", res.total) | ||||
| 
 | ||||
| 			if deletionSafe == res.total { | ||||
| 				if _, ok := getAnnotation(&res.statefulset.ObjectMeta, unregistrationCompleteTimestamp); !ok { | ||||
| 					updated := res.statefulset.DeepCopy() | ||||
| 					setAnnotation(&updated.ObjectMeta, unregistrationCompleteTimestamp, time.Now().Format(time.RFC3339)) | ||||
| 
 | ||||
| 					if err := r.Client.Patch(ctx, updated, client.MergeFrom(res.statefulset)); err != nil { | ||||
| 						log.Error(err, fmt.Sprintf("Failed to patch statefulset to have %s annotation", unregistrationCompleteTimestamp)) | ||||
| 						return ctrl.Result{}, err | ||||
| 					} | ||||
| 
 | ||||
| 					log.V(2).Info("Redundant statefulset has been annotated to start the deletion") | ||||
| 				} else { | ||||
| 					log.V(2).Info("BUG: Redundant statefulset was already annotated to start the deletion") | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if res.statefulset.Annotations != nil { | ||||
| 			if a, ok := res.statefulset.Annotations[SyncTimeAnnotationKey]; ok { | ||||
| 				t, err := time.Parse(time.RFC3339, a) | ||||
|  | @ -250,34 +294,58 @@ func (r *RunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( | |||
| 		log.V(2).Info("Created statefulset(s) to add more replicas", "num", num) | ||||
| 
 | ||||
| 		return ctrl.Result{}, nil | ||||
| 	} else if newDesiredReplicas < running { | ||||
| 	} else if newDesiredReplicas <= running { | ||||
| 		var retained int | ||||
| 		var lastIndex int | ||||
| 
 | ||||
| 		var delete []*podsForStatefulset | ||||
| 		for i := len(currentStatefulSets) - 1; i >= 0; i-- { | ||||
| 			ss := currentStatefulSets[i] | ||||
| 			retained += ss.running | ||||
| 			if retained >= newDesiredReplicas { | ||||
| 				lastIndex = i | ||||
| 				break | ||||
| 
 | ||||
| 			if ss.running == 0 || retained >= newDesiredReplicas { | ||||
| 				delete = append(delete, ss) | ||||
| 			} else if retained < newDesiredReplicas { | ||||
| 				retained += ss.running | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if retained == newDesiredReplicas { | ||||
| 			for i := 0; i < lastIndex; i++ { | ||||
| 				ss := currentStatefulSets[i] | ||||
| 			for _, ss := range delete { | ||||
| 				log := log.WithValues("statefulset", types.NamespacedName{Namespace: ss.statefulset.Namespace, Name: ss.statefulset.Name}) | ||||
| 				if err := r.Client.Delete(ctx, ss.statefulset); err != nil { | ||||
| 					return ctrl.Result{}, err | ||||
| 				// Statefulset termination process 1/4: Set unregistrationRequestTimestamp only after all the pods managed by the statefulset have
 | ||||
| 				// started unregistreation process.
 | ||||
| 				//
 | ||||
| 				// NOTE: We just mark it instead of immediately starting the deletion process.
 | ||||
| 				// Otherwise, the runner pod may hit termiationGracePeriod before the unregistration completes(the max terminationGracePeriod is limited to 1h by K8s and a job can be run for more than that),
 | ||||
| 				// or actions/runner may potentially misbehave on SIGTERM immediately sent by K8s.
 | ||||
| 				// We'd better unregister first and then start a pod deletion process.
 | ||||
| 				// The annotation works as a mark to start the pod unregistration and deletion process of ours.
 | ||||
| 				for _, po := range ss.pods { | ||||
| 					if _, err := annotatePodOnce(ctx, r.Client, log, &po, unregistrationRequestTimestamp, time.Now().Format(time.RFC3339)); err != nil { | ||||
| 						return ctrl.Result{}, err | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				if _, ok := getAnnotation(&ss.statefulset.ObjectMeta, unregistrationRequestTimestamp); !ok { | ||||
| 					updated := ss.statefulset.DeepCopy() | ||||
| 					setAnnotation(&updated.ObjectMeta, unregistrationRequestTimestamp, time.Now().Format(time.RFC3339)) | ||||
| 
 | ||||
| 					if err := r.Client.Patch(ctx, updated, client.MergeFrom(ss.statefulset)); err != nil { | ||||
| 						log.Error(err, fmt.Sprintf("Failed to patch statefulset to have %s annotation", unregistrationRequestTimestamp)) | ||||
| 						return ctrl.Result{}, err | ||||
| 					} | ||||
| 
 | ||||
| 					log.V(2).Info("Redundant statefulset has been annotated to start the unregistration before deletion") | ||||
| 				} else { | ||||
| 					log.V(2).Info("BUG: Redundant statefulset was already annotated") | ||||
| 				} | ||||
| 				log.V(2).Info("Deleted redundant statefulset", "i", i, "lastIndex", lastIndex) | ||||
| 			} | ||||
| 			return ctrl.Result{}, err | ||||
| 		} else if retained > newDesiredReplicas { | ||||
| 			log.V(2).Info("Waiting sync before scale down", "retained", retained, "newDesiredReplicas", newDesiredReplicas, "lastIndex", lastIndex) | ||||
| 			log.V(2).Info("Waiting sync before scale down", "retained", retained, "newDesiredReplicas", newDesiredReplicas) | ||||
| 
 | ||||
| 			return ctrl.Result{}, nil | ||||
| 		} else { | ||||
| 			log.Info("Invalid state", "retained", retained, "newDesiredReplicas", newDesiredReplicas, "lastIndex", lastIndex) | ||||
| 			log.Info("Invalid state", "retained", retained, "newDesiredReplicas", newDesiredReplicas) | ||||
| 			panic("crashed due to invalid state") | ||||
| 		} | ||||
| 	} | ||||
|  | @ -352,11 +420,15 @@ func (r *RunnerSetReconciler) getPodsForStatefulset(ctx context.Context, log log | |||
| 
 | ||||
| 	var completed, running, terminating, pending, total int | ||||
| 
 | ||||
| 	var pods []corev1.Pod | ||||
| 
 | ||||
| 	for _, pod := range podList.Items { | ||||
| 		if owner := metav1.GetControllerOf(&pod); owner == nil || owner.Kind != "StatefulSet" || owner.Name != ss.Name { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		pods = append(pods, pod) | ||||
| 
 | ||||
| 		total++ | ||||
| 
 | ||||
| 		if runnerPodOrContainerIsStopped(&pod) { | ||||
|  | @ -385,7 +457,7 @@ func (r *RunnerSetReconciler) getPodsForStatefulset(ctx context.Context, log log | |||
| 		pending:      pending, | ||||
| 		templateHash: templateHash, | ||||
| 		statefulset:  ss, | ||||
| 		pods:         podList.Items, | ||||
| 		pods:         pods, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue