Shutdown the scaleset when runner is deprecated (#4404)

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
This commit is contained in:
Nikola Jokic
2026-03-19 13:30:20 +01:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 40595d806f
commit 9bc1c9e53e
18 changed files with 411 additions and 180 deletions
@@ -315,7 +315,7 @@ type AutoscalingRunnerSetStatus struct {
CurrentRunners int `json:"currentRunners"` CurrentRunners int `json:"currentRunners"`
// +optional // +optional
State string `json:"state"` Phase AutoscalingRunnerSetPhase `json:"phase"`
// EphemeralRunner counts separated by the stage ephemeral runners are in, taken from the EphemeralRunnerSet // EphemeralRunner counts separated by the stage ephemeral runners are in, taken from the EphemeralRunnerSet
@@ -327,6 +327,30 @@ type AutoscalingRunnerSetStatus struct {
FailedEphemeralRunners int `json:"failedEphemeralRunners"` FailedEphemeralRunners int `json:"failedEphemeralRunners"`
} }
type AutoscalingRunnerSetPhase string
const (
// AutoscalingRunnerSetPhasePending phase means that the listener is not
// yet started
AutoscalingRunnerSetPhasePending AutoscalingRunnerSetPhase = "Pending"
AutoscalingRunnerSetPhaseRunning AutoscalingRunnerSetPhase = "Running"
AutoscalingRunnerSetPhaseOutdated AutoscalingRunnerSetPhase = "Outdated"
)
func (ars *AutoscalingRunnerSet) Hash() string {
type data struct {
Spec *AutoscalingRunnerSetSpec
Labels map[string]string
}
d := &data{
Spec: ars.Spec.DeepCopy(),
Labels: ars.Labels,
}
return hash.ComputeTemplateHash(d)
}
func (ars *AutoscalingRunnerSet) ListenerSpecHash() string { func (ars *AutoscalingRunnerSet) ListenerSpecHash() string {
arsSpec := ars.Spec.DeepCopy() arsSpec := ars.Spec.DeepCopy()
spec := arsSpec spec := arsSpec
@@ -48,7 +48,7 @@ type EphemeralRunner struct {
} }
func (er *EphemeralRunner) IsDone() bool { func (er *EphemeralRunner) IsDone() bool {
return er.Status.Phase == corev1.PodSucceeded || er.Status.Phase == corev1.PodFailed return er.Status.Phase == EphemeralRunnerPhaseSucceeded || er.Status.Phase == EphemeralRunnerPhaseFailed || er.Status.Phase == EphemeralRunnerPhaseOutdated
} }
func (er *EphemeralRunner) HasJob() bool { func (er *EphemeralRunner) HasJob() bool {
@@ -143,14 +143,14 @@ type EphemeralRunnerStatus struct {
// The PodSucceded phase should be set only when confirmed that EphemeralRunner // The PodSucceded phase should be set only when confirmed that EphemeralRunner
// actually executed the job and has been removed from the service. // actually executed the job and has been removed from the service.
// +optional // +optional
Phase corev1.PodPhase `json:"phase,omitempty"` Phase EphemeralRunnerPhase `json:"phase,omitempty"`
// +optional // +optional
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
// +optional // +optional
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
// +optional // +optional
RunnerId int `json:"runnerId,omitempty"` RunnerID int `json:"runnerId,omitempty"`
// +optional // +optional
RunnerName string `json:"runnerName,omitempty"` RunnerName string `json:"runnerName,omitempty"`
@@ -158,7 +158,7 @@ type EphemeralRunnerStatus struct {
Failures map[string]metav1.Time `json:"failures,omitempty"` Failures map[string]metav1.Time `json:"failures,omitempty"`
// +optional // +optional
JobRequestId int64 `json:"jobRequestId,omitempty"` JobRequestID int64 `json:"jobRequestId,omitempty"`
// +optional // +optional
JobID string `json:"jobId,omitempty"` JobID string `json:"jobId,omitempty"`
@@ -170,12 +170,33 @@ type EphemeralRunnerStatus struct {
JobWorkflowRef string `json:"jobWorkflowRef,omitempty"` JobWorkflowRef string `json:"jobWorkflowRef,omitempty"`
// +optional // +optional
WorkflowRunId int64 `json:"workflowRunId,omitempty"` WorkflowRunID int64 `json:"workflowRunId,omitempty"`
// +optional // +optional
JobDisplayName string `json:"jobDisplayName,omitempty"` JobDisplayName string `json:"jobDisplayName,omitempty"`
} }
// EphemeralRunnerPhase is the phase of the ephemeral runner.
// It must be a superset of the pod phase.
type EphemeralRunnerPhase string
const (
// EphemeralRunnerPhasePending is a phase set when the ephemeral runner is
// being provisioned and is not yet online.
EphemeralRunnerPhasePending EphemeralRunnerPhase = "Pending"
// EphemeralRunnerPhaseRunning is a phase set when the ephemeral runner is online and
// waiting for a job to execute.
EphemeralRunnerPhaseRunning EphemeralRunnerPhase = "Running"
// EphemeralRunnerPhaseSucceeded is a phase set when the ephemeral runner
// successfully executed the job and has been removed from the service.
EphemeralRunnerPhaseSucceeded EphemeralRunnerPhase = "Succeeded"
// EphemeralRunnerPhaseFailed is a phase set when the ephemeral runner
// fails with unrecoverable failure.
EphemeralRunnerPhaseFailed EphemeralRunnerPhase = "Failed"
// EphemeralRunnerPhaseOutdated is a special phase that indicates the runner is outdated and should be upgraded.
EphemeralRunnerPhaseOutdated EphemeralRunnerPhase = "Outdated"
)
func (s *EphemeralRunnerStatus) LastFailure() metav1.Time { func (s *EphemeralRunnerStatus) LastFailure() metav1.Time {
var maxTime metav1.Time var maxTime metav1.Time
if len(s.Failures) == 0 { if len(s.Failures) == 0 {
@@ -42,8 +42,20 @@ type EphemeralRunnerSetStatus struct {
RunningEphemeralRunners int `json:"runningEphemeralRunners"` RunningEphemeralRunners int `json:"runningEphemeralRunners"`
// +optional // +optional
FailedEphemeralRunners int `json:"failedEphemeralRunners"` FailedEphemeralRunners int `json:"failedEphemeralRunners"`
// +optional
Phase EphemeralRunnerSetPhase `json:"phase"`
} }
// EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource
type EphemeralRunnerSetPhase string
const (
EphemeralRunnerSetPhaseRunning EphemeralRunnerSetPhase = "Running"
// EphemeralRunnerSetPhaseOutdated is set when at least one ephemeral runner
// contains the outdated phase
EphemeralRunnerSetPhaseOutdated EphemeralRunnerSetPhase = "Outdated"
)
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
// +kubebuilder:subresource:status // +kubebuilder:subresource:status
// +kubebuilder:printcolumn:JSONPath=".spec.replicas",name="DesiredReplicas",type="integer" // +kubebuilder:printcolumn:JSONPath=".spec.replicas",name="DesiredReplicas",type="integer"
@@ -16547,10 +16547,10 @@ spec:
type: integer type: integer
pendingEphemeralRunners: pendingEphemeralRunners:
type: integer type: integer
phase:
type: string
runningEphemeralRunners: runningEphemeralRunners:
type: integer type: integer
state:
type: string
type: object type: object
type: object type: object
served: true served: true
@@ -8318,6 +8318,9 @@ spec:
type: integer type: integer
pendingEphemeralRunners: pendingEphemeralRunners:
type: integer type: integer
phase:
description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource
type: string
runningEphemeralRunners: runningEphemeralRunners:
type: integer type: integer
required: required:
@@ -16547,10 +16547,10 @@ spec:
type: integer type: integer
pendingEphemeralRunners: pendingEphemeralRunners:
type: integer type: integer
phase:
type: string
runningEphemeralRunners: runningEphemeralRunners:
type: integer type: integer
state:
type: string
type: object type: object
type: object type: object
served: true served: true
@@ -8318,6 +8318,9 @@ spec:
type: integer type: integer
pendingEphemeralRunners: pendingEphemeralRunners:
type: integer type: integer
phase:
description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource
type: string
runningEphemeralRunners: runningEphemeralRunners:
type: integer type: integer
required: required:
+2 -2
View File
@@ -110,10 +110,10 @@ func (w *Scaler) HandleJobStarted(ctx context.Context, jobInfo *scaleset.JobStar
patch, err := json.Marshal( patch, err := json.Marshal(
&v1alpha1.EphemeralRunner{ &v1alpha1.EphemeralRunner{
Status: v1alpha1.EphemeralRunnerStatus{ Status: v1alpha1.EphemeralRunnerStatus{
JobRequestId: jobInfo.RunnerRequestID, JobRequestID: jobInfo.RunnerRequestID,
JobRepositoryName: fmt.Sprintf("%s/%s", jobInfo.OwnerName, jobInfo.RepositoryName), JobRepositoryName: fmt.Sprintf("%s/%s", jobInfo.OwnerName, jobInfo.RepositoryName),
JobID: jobInfo.JobID, JobID: jobInfo.JobID,
WorkflowRunId: jobInfo.WorkflowRunID, WorkflowRunID: jobInfo.WorkflowRunID,
JobWorkflowRef: jobInfo.JobWorkflowRef, JobWorkflowRef: jobInfo.JobWorkflowRef,
JobDisplayName: jobInfo.JobDisplayName, JobDisplayName: jobInfo.JobDisplayName,
}, },
@@ -16547,10 +16547,10 @@ spec:
type: integer type: integer
pendingEphemeralRunners: pendingEphemeralRunners:
type: integer type: integer
phase:
type: string
runningEphemeralRunners: runningEphemeralRunners:
type: integer type: integer
state:
type: string
type: object type: object
type: object type: object
served: true served: true
@@ -8318,6 +8318,9 @@ spec:
type: integer type: integer
pendingEphemeralRunners: pendingEphemeralRunners:
type: integer type: integer
phase:
description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource
type: string
runningEphemeralRunners: runningEphemeralRunners:
type: integer type: integer
required: required:
@@ -22,6 +22,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/build" "github.com/actions/actions-runner-controller/build"
@@ -46,6 +47,7 @@ const (
// This is used to determine if the values have changed, so we can // This is used to determine if the values have changed, so we can
// re-create listener. // re-create listener.
annotationKeyValuesHash = "actions.github.com/values-hash" annotationKeyValuesHash = "actions.github.com/values-hash"
annotationKeyChangeHash = "actions.github.com/change-hash"
autoscalingRunnerSetFinalizerName = "autoscalingrunnerset.actions.github.com/finalizer" autoscalingRunnerSetFinalizerName = "autoscalingrunnerset.actions.github.com/finalizer"
runnerScaleSetIDAnnotationKey = "runner-scale-set-id" runnerScaleSetIDAnnotationKey = "runner-scale-set-id"
@@ -104,32 +106,16 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
} }
log.Info("Deleting resources") log.Info("Deleting resources")
done, err := r.cleanupListener(ctx, autoscalingRunnerSet, log) done, err := r.cleanUpResources(ctx, autoscalingRunnerSet, nil, log)
if err != nil { if err != nil {
log.Error(err, "Failed to clean up listener") log.Error(err, "Failed to clean up resources during deletion")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
if !done { if !done {
// we are going to get notified anyway to proceed with rest of the log.Info("Waiting for resources to be cleaned up before removing finalizer")
// cleanup. No need to re-queue return ctrl.Result{
log.Info("Waiting for listener to be deleted") RequeueAfter: 5 * time.Second,
return ctrl.Result{}, nil }, nil
}
done, err = r.cleanupEphemeralRunnerSets(ctx, autoscalingRunnerSet, log)
if err != nil {
log.Error(err, "Failed to clean up ephemeral runner sets")
return ctrl.Result{}, err
}
if !done {
log.Info("Waiting for ephemeral runner sets to be deleted")
return ctrl.Result{}, nil
}
err = r.deleteRunnerScaleSet(ctx, autoscalingRunnerSet, log)
if err != nil {
log.Error(err, "Failed to delete runner scale set")
return ctrl.Result{}, err
} }
if err := r.removeFinalizersFromDependentResources(ctx, autoscalingRunnerSet, log); err != nil { if err := r.removeFinalizersFromDependentResources(ctx, autoscalingRunnerSet, log); err != nil {
@@ -176,34 +162,54 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
} }
log.Info("Successfully added finalizer") log.Info("Successfully added finalizer")
return ctrl.Result{}, nil
} }
scaleSetIDRaw, ok := autoscalingRunnerSet.Annotations[runnerScaleSetIDAnnotationKey] if targetHash := autoscalingRunnerSet.Hash(); autoscalingRunnerSet.Annotations[annotationKeyChangeHash] != targetHash {
if !ok { if err := patch(ctx, r.Client, autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
// Need to create a new runner scale set on Actions service if obj.Annotations == nil {
log.Info("Runner scale set id annotation does not exist. Creating a new runner scale set.") obj.Annotations = map[string]string{}
return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log) }
obj.Annotations[annotationKeyChangeHash] = targetHash
}); err != nil {
log.Error(err, "Failed to update autoscaling runner set with change hash annotation")
return ctrl.Result{}, err
}
if err := r.updateStatus(ctx, autoscalingRunnerSet, nil, v1alpha1.AutoscalingRunnerSetPhasePending, log); err != nil {
log.Error(err, "Failed to update autoscaling runner set status to pending")
return ctrl.Result{}, err
}
} }
if id, err := strconv.Atoi(scaleSetIDRaw); err != nil || id <= 0 { outdated := autoscalingRunnerSet.Status.Phase == v1alpha1.AutoscalingRunnerSetPhaseOutdated
log.Info("Runner scale set id annotation is not an id, or is <= 0. Creating a new runner scale set.")
// something modified the scaleSetId. Try to create one
return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log)
}
// Make sure the runner group of the scale set is up to date if !outdated {
currentRunnerGroupName, ok := autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerGroupName] scaleSetIDRaw, ok := autoscalingRunnerSet.Annotations[runnerScaleSetIDAnnotationKey]
if !ok || (len(autoscalingRunnerSet.Spec.RunnerGroup) > 0 && !strings.EqualFold(currentRunnerGroupName, autoscalingRunnerSet.Spec.RunnerGroup)) { if !ok {
log.Info("AutoScalingRunnerSet runner group changed. Updating the runner scale set.") // Need to create a new runner scale set on Actions service
return r.updateRunnerScaleSetRunnerGroup(ctx, autoscalingRunnerSet, log) log.Info("Runner scale set id annotation does not exist. Creating a new runner scale set.")
} return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log)
}
// Make sure the runner scale set name is up to date if id, err := strconv.Atoi(scaleSetIDRaw); err != nil || id <= 0 {
currentRunnerScaleSetName, ok := autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName] log.Info("Runner scale set id annotation is not an id, or is <= 0. Creating a new runner scale set.")
if !ok || (len(autoscalingRunnerSet.Spec.RunnerScaleSetName) > 0 && !strings.EqualFold(currentRunnerScaleSetName, autoscalingRunnerSet.Spec.RunnerScaleSetName)) { // something modified the scaleSetId. Try to create one
log.Info("AutoScalingRunnerSet runner scale set name changed. Updating the runner scale set.") return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log)
return r.updateRunnerScaleSetName(ctx, autoscalingRunnerSet, log) }
// Make sure the runner group of the scale set is up to date
currentRunnerGroupName, ok := autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerGroupName]
if !ok || (len(autoscalingRunnerSet.Spec.RunnerGroup) > 0 && !strings.EqualFold(currentRunnerGroupName, autoscalingRunnerSet.Spec.RunnerGroup)) {
log.Info("AutoScalingRunnerSet runner group changed. Updating the runner scale set.")
return r.updateRunnerScaleSetRunnerGroup(ctx, autoscalingRunnerSet, log)
}
// Make sure the runner scale set name is up to date
currentRunnerScaleSetName, ok := autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName]
if !ok || (len(autoscalingRunnerSet.Spec.RunnerScaleSetName) > 0 && !strings.EqualFold(currentRunnerScaleSetName, autoscalingRunnerSet.Spec.RunnerScaleSetName)) {
log.Info("AutoScalingRunnerSet runner scale set name changed. Updating the runner scale set.")
return r.updateRunnerScaleSetName(ctx, autoscalingRunnerSet, log)
}
} }
existingRunnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet) existingRunnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet)
@@ -213,7 +219,7 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
} }
latestRunnerSet := existingRunnerSets.latest() latestRunnerSet := existingRunnerSets.latest()
if latestRunnerSet == nil { if latestRunnerSet == nil && !outdated {
log.Info("Latest runner set does not exist. Creating a new runner set.") log.Info("Latest runner set does not exist. Creating a new runner set.")
return r.createEphemeralRunnerSet(ctx, autoscalingRunnerSet, log) return r.createEphemeralRunnerSet(ctx, autoscalingRunnerSet, log)
} }
@@ -222,6 +228,8 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
log.Info("Find existing ephemeral runner set", "name", runnerSet.Name, "specHash", runnerSet.Annotations[annotationKeyRunnerSpecHash]) log.Info("Find existing ephemeral runner set", "name", runnerSet.Name, "specHash", runnerSet.Annotations[annotationKeyRunnerSpecHash])
} }
outdated = outdated || (latestRunnerSet != nil && latestRunnerSet.Status.Phase == v1alpha1.EphemeralRunnerSetPhaseOutdated)
// Make sure the AutoscalingListener is up and running in the controller namespace // Make sure the AutoscalingListener is up and running in the controller namespace
listener := new(v1alpha1.AutoscalingListener) listener := new(v1alpha1.AutoscalingListener)
listenerFound := true listenerFound := true
@@ -235,6 +243,30 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
log.Info("AutoscalingListener does not exist.") log.Info("AutoscalingListener does not exist.")
} }
if outdated {
log.Info("Ephemeral runner set is outdated")
if autoscalingRunnerSet.Status.Phase != v1alpha1.AutoscalingRunnerSetPhaseOutdated {
if err := r.updateStatus(ctx, autoscalingRunnerSet, latestRunnerSet, v1alpha1.AutoscalingRunnerSetPhaseOutdated, log); err != nil {
log.Error(err, "Failed to update autoscaling runner set status to outdated")
return ctrl.Result{}, err
}
}
done, err := r.cleanUpResources(ctx, autoscalingRunnerSet, latestRunnerSet, log)
if err != nil {
log.Error(err, "Failed to clean up resources for outdated runner set")
return ctrl.Result{}, err
}
if done {
return ctrl.Result{}, nil
}
log.Info("Waiting for resources to be cleaned up for outdated runner set")
return ctrl.Result{
RequeueAfter: 5 * time.Second,
}, nil
}
// Our listener pod is out of date, so we need to delete it to get a new recreate. // Our listener pod is out of date, so we need to delete it to get a new recreate.
listenerValuesHashChanged := listener.Annotations[annotationKeyValuesHash] != autoscalingRunnerSet.Annotations[annotationKeyValuesHash] listenerValuesHashChanged := listener.Annotations[annotationKeyValuesHash] != autoscalingRunnerSet.Annotations[annotationKeyValuesHash]
listenerSpecHashChanged := listener.Annotations[annotationKeyRunnerSpecHash] != autoscalingRunnerSet.ListenerSpecHash() listenerSpecHashChanged := listener.Annotations[annotationKeyRunnerSpecHash] != autoscalingRunnerSet.ListenerSpecHash()
@@ -291,22 +323,71 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, log) return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, log)
} }
// Update the status of autoscaling runner set. if err := r.updateStatus(ctx, autoscalingRunnerSet, latestRunnerSet, v1alpha1.AutoscalingRunnerSetPhaseRunning, log); err != nil {
if latestRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners { log.Error(err, "Failed to update autoscaling runner set status to running")
if err := patchSubResource(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) { return ctrl.Result{}, err
obj.Status.CurrentRunners = latestRunnerSet.Status.CurrentReplicas
obj.Status.PendingEphemeralRunners = latestRunnerSet.Status.PendingEphemeralRunners
obj.Status.RunningEphemeralRunners = latestRunnerSet.Status.RunningEphemeralRunners
obj.Status.FailedEphemeralRunners = latestRunnerSet.Status.FailedEphemeralRunners
}); err != nil {
log.Error(err, "Failed to update autoscaling runner set status with current runner count")
return ctrl.Result{}, err
}
} }
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
func (r *AutoscalingRunnerSetReconciler) cleanUpResources(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, latestRunnerSet *v1alpha1.EphemeralRunnerSet, log logr.Logger) (bool, error) {
log.Info("Deleting the listener")
done, err := r.cleanupListener(ctx, autoscalingRunnerSet, log)
if err != nil {
log.Error(err, "Failed to clean up listener")
return false, err
}
if !done {
log.Info("Waiting for listener to be deleted")
return false, nil
}
log.Info("deleting ephemeral runner sets")
done, err = r.cleanupEphemeralRunnerSets(ctx, autoscalingRunnerSet, log)
if err != nil {
log.Error(err, "Failed to clean up ephemeral runner sets")
return false, err
}
if !done {
log.Info("Waiting for ephemeral runner sets to be deleted")
return false, nil
}
log.Info("deleting runner scale set")
err = r.deleteRunnerScaleSet(ctx, autoscalingRunnerSet, log)
if err != nil {
log.Error(err, "Failed to delete runner scale set")
return false, err
}
return true, nil
}
// Update the status of autoscaling runner set if necessary
func (r *AutoscalingRunnerSetReconciler) updateStatus(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, phase v1alpha1.AutoscalingRunnerSetPhase, log logr.Logger) error {
countDiff := ephemeralRunnerSet != nil && ephemeralRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners
phaseDiff := phase != autoscalingRunnerSet.Status.Phase
if countDiff || phaseDiff {
if err := patchSubResource(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
obj.Status.Phase = phase
var ephemeralRunnerSetStatus v1alpha1.EphemeralRunnerSetStatus
if ephemeralRunnerSet != nil {
ephemeralRunnerSetStatus = ephemeralRunnerSet.Status
}
obj.Status.CurrentRunners = ephemeralRunnerSetStatus.CurrentReplicas
obj.Status.PendingEphemeralRunners = ephemeralRunnerSetStatus.PendingEphemeralRunners
obj.Status.RunningEphemeralRunners = ephemeralRunnerSetStatus.RunningEphemeralRunners
obj.Status.FailedEphemeralRunners = ephemeralRunnerSetStatus.FailedEphemeralRunners
}); err != nil {
log.Error(err, "Failed to update autoscaling runner set status with current runner count")
return err
}
}
return nil
}
// Prevents overprovisioning of runners. // Prevents overprovisioning of runners.
// We reach this code path when runner scale set has been patched with a new runner spec but there are still running ephemeral runners. // We reach this code path when runner scale set has been patched with a new runner spec but there are still running ephemeral runners.
// The safest approach is to wait for the running ephemeral runners to finish before creating a new runner set. // The safest approach is to wait for the running ephemeral runners to finish before creating a new runner set.
@@ -171,10 +171,18 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
return "", nil return "", nil
} }
return fmt.Sprintf("%s_%s", created.Annotations[runnerScaleSetIDAnnotationKey], created.Annotations[AnnotationKeyGitHubRunnerGroupName]), nil return fmt.Sprintf(
"%s_%s",
created.Annotations[runnerScaleSetIDAnnotationKey],
created.Annotations[AnnotationKeyGitHubRunnerGroupName],
), nil
}, },
autoscalingRunnerSetTestTimeout, autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval).Should(BeEquivalentTo("1_testgroup"), "RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's annotation") autoscalingRunnerSetTestInterval,
).Should(
BeEquivalentTo("1_testgroup"),
"RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's annotation",
)
Eventually( Eventually(
func() (string, error) { func() (string, error) {
@@ -548,7 +556,7 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{ desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{
CurrentRunners: activeRunnerSet.Status.CurrentReplicas, CurrentRunners: activeRunnerSet.Status.CurrentReplicas,
State: "", Phase: v1alpha1.AutoscalingRunnerSetPhaseRunning,
PendingEphemeralRunners: activeRunnerSet.Status.PendingEphemeralRunners, PendingEphemeralRunners: activeRunnerSet.Status.PendingEphemeralRunners,
RunningEphemeralRunners: activeRunnerSet.Status.RunningEphemeralRunners, RunningEphemeralRunners: activeRunnerSet.Status.RunningEphemeralRunners,
FailedEphemeralRunners: activeRunnerSet.Status.FailedEphemeralRunners, FailedEphemeralRunners: activeRunnerSet.Status.FailedEphemeralRunners,
@@ -654,7 +662,7 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{ desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{
CurrentRunners: statusUpdate.Status.CurrentReplicas, CurrentRunners: statusUpdate.Status.CurrentReplicas,
State: "", Phase: v1alpha1.AutoscalingRunnerSetPhaseRunning,
PendingEphemeralRunners: statusUpdate.Status.PendingEphemeralRunners, PendingEphemeralRunners: statusUpdate.Status.PendingEphemeralRunners,
RunningEphemeralRunners: statusUpdate.Status.RunningEphemeralRunners, RunningEphemeralRunners: statusUpdate.Status.RunningEphemeralRunners,
FailedEphemeralRunners: statusUpdate.Status.FailedEphemeralRunners, FailedEphemeralRunners: statusUpdate.Status.FailedEphemeralRunners,
@@ -12,6 +12,9 @@ const (
const ( const (
EnvVarRunnerJITConfig = "ACTIONS_RUNNER_INPUT_JITCONFIG" EnvVarRunnerJITConfig = "ACTIONS_RUNNER_INPUT_JITCONFIG"
EnvVarRunnerExtraUserAgent = "GITHUB_ACTIONS_RUNNER_EXTRA_USER_AGENT" EnvVarRunnerExtraUserAgent = "GITHUB_ACTIONS_RUNNER_EXTRA_USER_AGENT"
// Environment variable setting the exit code to return when the runner version is deprecated.
// This is used by the runner to signal to the controller that it should switch off the scaleset.
EnvVarRunnerDeprecatedExitCode = "ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE"
) )
// Environment variable names used to set proxy variables for containers // Environment variable names used to set proxy variables for containers
@@ -201,7 +201,7 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
} }
} }
if ephemeralRunner.Status.RunnerId == 0 { if ephemeralRunner.Status.RunnerID == 0 {
log.Info("Updating ephemeral runner status with runnerId and runnerName") log.Info("Updating ephemeral runner status with runnerId and runnerName")
runnerID, err := strconv.Atoi(string(secret.Data["runnerId"])) runnerID, err := strconv.Atoi(string(secret.Data["runnerId"]))
if err != nil { if err != nil {
@@ -216,12 +216,12 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
runnerName := string(secret.Data["runnerName"]) runnerName := string(secret.Data["runnerName"])
if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) { if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
obj.Status.RunnerId = runnerID obj.Status.RunnerID = runnerID
obj.Status.RunnerName = runnerName obj.Status.RunnerName = runnerName
}); err != nil { }); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to update runner status for RunnerId/RunnerName/RunnerJITConfig: %w", err) return ctrl.Result{}, fmt.Errorf("failed to update runner status for RunnerId/RunnerName/RunnerJITConfig: %w", err)
} }
ephemeralRunner.Status.RunnerId = runnerID ephemeralRunner.Status.RunnerID = runnerID
ephemeralRunner.Status.RunnerName = runnerName ephemeralRunner.Status.RunnerName = runnerName
log.Info("Updated ephemeral runner status with runnerId and runnerName") log.Info("Updated ephemeral runner status with runnerId and runnerName")
} }
@@ -324,7 +324,8 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{}, r.deleteEphemeralRunnerOrPod(ctx, ephemeralRunner, pod, log) return ctrl.Result{}, r.deleteEphemeralRunnerOrPod(ctx, ephemeralRunner, pod, log)
} }
if cs.State.Terminated.ExitCode == 0 { switch cs.State.Terminated.ExitCode {
case 0:
log.Info("Runner container has succeeded but pod is in failed phase; Assume successful exit") log.Info("Runner container has succeeded but pod is in failed phase; Assume successful exit")
// If the pod is in a failed state, that means that at least one container exited with non-zero exit code. // If the pod is in a failed state, that means that at least one container exited with non-zero exit code.
// If the runner container exits with 0, we assume that the runner has finished successfully. // If the runner container exits with 0, we assume that the runner has finished successfully.
@@ -335,6 +336,12 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{}, err return ctrl.Result{}, err
} }
return ctrl.Result{}, nil return ctrl.Result{}, nil
case 7:
if err := r.markAsOutdated(ctx, ephemeralRunner, log); err != nil {
log.Error(err, "Failed to set ephemeral runner to phase Outdated")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
} }
log.Error( log.Error(
@@ -357,6 +364,13 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
} }
return ctrl.Result{}, nil return ctrl.Result{}, nil
case cs.State.Terminated.ExitCode == 7: // outdated
if err := r.markAsOutdated(ctx, ephemeralRunner, log); err != nil {
log.Error(err, "Failed to set ephemeral runner to phase Outdated")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
case cs.State.Terminated.ExitCode != 0: // failed case cs.State.Terminated.ExitCode != 0: // failed
log.Info("Ephemeral runner container failed", "exitCode", cs.State.Terminated.ExitCode) log.Info("Ephemeral runner container failed", "exitCode", cs.State.Terminated.ExitCode)
return ctrl.Result{}, r.deleteEphemeralRunnerOrPod(ctx, ephemeralRunner, pod, log) return ctrl.Result{}, r.deleteEphemeralRunnerOrPod(ctx, ephemeralRunner, pod, log)
@@ -390,7 +404,7 @@ func (r *EphemeralRunnerReconciler) deleteEphemeralRunnerOrPod(ctx context.Conte
log.Error(err, "Failed to get actions client for removing the runner from the service") log.Error(err, "Failed to get actions client for removing the runner from the service")
return nil return nil
} }
if err := actionsClient.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerId)); err != nil { if err := actionsClient.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerID)); err != nil {
log.Error(err, "Failed to remove the runner from the service") log.Error(err, "Failed to remove the runner from the service")
return nil return nil
} }
@@ -549,7 +563,7 @@ func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedSecrets(ctx context.Conte
func (r *EphemeralRunnerReconciler) markAsFailed(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, errMessage string, reason string, log logr.Logger) error { func (r *EphemeralRunnerReconciler) markAsFailed(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, errMessage string, reason string, log logr.Logger) error {
log.Info("Updating ephemeral runner status to Failed") log.Info("Updating ephemeral runner status to Failed")
if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) { if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
obj.Status.Phase = corev1.PodFailed obj.Status.Phase = v1alpha1.EphemeralRunnerPhaseFailed
obj.Status.Reason = reason obj.Status.Reason = reason
obj.Status.Message = errMessage obj.Status.Message = errMessage
}); err != nil { }); err != nil {
@@ -565,6 +579,24 @@ func (r *EphemeralRunnerReconciler) markAsFailed(ctx context.Context, ephemeralR
return nil return nil
} }
func (r *EphemeralRunnerReconciler) markAsOutdated(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error {
log.Info("Updating ephemeral runner status to Outdated")
if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
obj.Status.Phase = v1alpha1.EphemeralRunnerPhaseOutdated
obj.Status.Reason = "Outdated"
obj.Status.Message = "Runner is deprecated"
}); err != nil {
return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %w", err)
}
log.Info("Removing the runner from the service")
if err := r.deleteRunnerFromService(ctx, ephemeralRunner, log); err != nil {
return fmt.Errorf("failed to remove the runner from service: %w", err)
}
return nil
}
// deletePodAsFailed is responsible for deleting the pod and updating the .Status.Failures for tracking failure count. // deletePodAsFailed is responsible for deleting the pod and updating the .Status.Failures for tracking failure count.
// It should not be responsible for setting the status to Failed. // It should not be responsible for setting the status to Failed.
// //
@@ -722,7 +754,7 @@ func (r *EphemeralRunnerReconciler) createPod(ctx context.Context, runner *v1alp
log.Info("Created ephemeral runner pod", log.Info("Created ephemeral runner pod",
"runnerScaleSetId", runner.Spec.RunnerScaleSetID, "runnerScaleSetId", runner.Spec.RunnerScaleSetID,
"runnerName", runner.Status.RunnerName, "runnerName", runner.Status.RunnerName,
"runnerId", runner.Status.RunnerId, "runnerId", runner.Status.RunnerID,
"configUrl", runner.Spec.GitHubConfigUrl, "configUrl", runner.Spec.GitHubConfigUrl,
"podName", newPod.Name) "podName", newPod.Name)
@@ -765,7 +797,8 @@ func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context,
} }
} }
phaseChanged := ephemeralRunner.Status.Phase != pod.Status.Phase phase := v1alpha1.EphemeralRunnerPhase(pod.Status.Phase)
phaseChanged := ephemeralRunner.Status.Phase != phase
readyChanged := ready != ephemeralRunner.Status.Ready readyChanged := ready != ephemeralRunner.Status.Ready
if !phaseChanged && !readyChanged { if !phaseChanged && !readyChanged {
@@ -780,7 +813,7 @@ func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context,
"ready", ready, "ready", ready,
) )
err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) { err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
obj.Status.Phase = pod.Status.Phase obj.Status.Phase = phase
obj.Status.Ready = ready obj.Status.Ready = ready
obj.Status.Reason = pod.Status.Reason obj.Status.Reason = pod.Status.Reason
obj.Status.Message = pod.Status.Message obj.Status.Message = pod.Status.Message
@@ -799,13 +832,13 @@ func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context,
return fmt.Errorf("failed to get actions client for runner: %w", err) return fmt.Errorf("failed to get actions client for runner: %w", err)
} }
log.Info("Removing runner from the service", "runnerId", ephemeralRunner.Status.RunnerId) log.Info("Removing runner from the service", "runnerId", ephemeralRunner.Status.RunnerID)
err = client.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerId)) err = client.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerID))
if err != nil { if err != nil {
return fmt.Errorf("failed to remove runner from the service: %w", err) return fmt.Errorf("failed to remove runner from the service: %w", err)
} }
log.Info("Removed runner from the service", "runnerId", ephemeralRunner.Status.RunnerId) log.Info("Removed runner from the service", "runnerId", ephemeralRunner.Status.RunnerID)
return nil return nil
} }
@@ -511,7 +511,7 @@ var _ = Describe("EphemeralRunner", func() {
updated := new(v1alpha1.EphemeralRunner) updated := new(v1alpha1.EphemeralRunner)
Eventually( Eventually(
func() (corev1.PodPhase, error) { func() (v1alpha1.EphemeralRunnerPhase, error) {
err := k8sClient.Get( err := k8sClient.Get(
ctx, ctx,
client.ObjectKey{Name: invalideEphemeralRunner.Name, Namespace: invalideEphemeralRunner.Namespace}, client.ObjectKey{Name: invalideEphemeralRunner.Name, Namespace: invalideEphemeralRunner.Namespace},
@@ -524,7 +524,7 @@ var _ = Describe("EphemeralRunner", func() {
}, },
ephemeralRunnerTimeout, ephemeralRunnerTimeout,
ephemeralRunnerInterval, ephemeralRunnerInterval,
).Should(BeEquivalentTo(corev1.PodFailed)) ).Should(BeEquivalentTo(v1alpha1.EphemeralRunnerPhaseFailed))
Expect(updated.Status.Reason).Should(Equal("InvalidPod")) Expect(updated.Status.Reason).Should(Equal("InvalidPod"))
Expect(updated.Status.Message).Should(Equal("Failed to create the pod: pods \"invalid-ephemeral-runner\" is forbidden: no PriorityClass with name notexist was found")) Expect(updated.Status.Message).Should(Equal("Failed to create the pod: pods \"invalid-ephemeral-runner\" is forbidden: no PriorityClass with name notexist was found"))
@@ -676,7 +676,7 @@ var _ = Describe("EphemeralRunner", func() {
if err != nil { if err != nil {
return 0, err return 0, err
} }
return updatedEphemeralRunner.Status.RunnerId, nil return updatedEphemeralRunner.Status.RunnerID, nil
}, },
ephemeralRunnerTimeout, ephemeralRunnerTimeout,
ephemeralRunnerInterval, ephemeralRunnerInterval,
@@ -711,7 +711,7 @@ var _ = Describe("EphemeralRunner", func() {
var updated *v1alpha1.EphemeralRunner var updated *v1alpha1.EphemeralRunner
Eventually( Eventually(
func() (corev1.PodPhase, error) { func() (v1alpha1.EphemeralRunnerPhase, error) {
updated = new(v1alpha1.EphemeralRunner) updated = new(v1alpha1.EphemeralRunner)
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
if err != nil { if err != nil {
@@ -833,10 +833,10 @@ var _ = Describe("EphemeralRunner", func() {
Expect(err).To(BeNil(), "failed to patch pod status") Expect(err).To(BeNil(), "failed to patch pod status")
Consistently( Consistently(
func() (corev1.PodPhase, error) { func() (v1alpha1.EphemeralRunnerPhase, error) {
updated := new(v1alpha1.EphemeralRunner) updated := new(v1alpha1.EphemeralRunner)
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil {
return corev1.PodUnknown, err return "Unknown", err
} }
return updated.Status.Phase, nil return updated.Status.Phase, nil
}, },
@@ -1059,7 +1059,7 @@ var _ = Describe("EphemeralRunner", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Eventually( Eventually(
func() (corev1.PodPhase, error) { func() (v1alpha1.EphemeralRunnerPhase, error) {
updated := new(v1alpha1.EphemeralRunner) updated := new(v1alpha1.EphemeralRunner)
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil {
return "", err return "", err
@@ -1068,7 +1068,7 @@ var _ = Describe("EphemeralRunner", func() {
}, },
ephemeralRunnerTimeout, ephemeralRunnerTimeout,
ephemeralRunnerInterval, ephemeralRunnerInterval,
).Should(BeEquivalentTo(corev1.PodRunning)) ).Should(BeEquivalentTo(v1alpha1.EphemeralRunnerPhaseRunning))
// set phase to succeeded // set phase to succeeded
pod.Status.Phase = corev1.PodSucceeded pod.Status.Phase = corev1.PodSucceeded
@@ -1076,7 +1076,7 @@ var _ = Describe("EphemeralRunner", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Consistently( Consistently(
func() (corev1.PodPhase, error) { func() (v1alpha1.EphemeralRunnerPhase, error) {
updated := new(v1alpha1.EphemeralRunner) updated := new(v1alpha1.EphemeralRunner)
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil {
return "", err return "", err
@@ -1084,7 +1084,7 @@ var _ = Describe("EphemeralRunner", func() {
return updated.Status.Phase, nil return updated.Status.Phase, nil
}, },
ephemeralRunnerTimeout, ephemeralRunnerTimeout,
).Should(BeEquivalentTo(corev1.PodRunning)) ).Should(BeEquivalentTo(v1alpha1.EphemeralRunnerPhaseRunning))
}) })
}) })
@@ -125,6 +125,11 @@ func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.R
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
if ephemeralRunnerSet.Status.Phase == v1alpha1.EphemeralRunnerSetPhaseOutdated {
log.Info("ephemeral runner set is outdated, waiting for autoscaling runner set to remove it")
return ctrl.Result{}, nil
}
// Create proxy secret if not present // Create proxy secret if not present
if ephemeralRunnerSet.Spec.EphemeralRunnerSpec.Proxy != nil { if ephemeralRunnerSet.Spec.EphemeralRunnerSpec.Proxy != nil {
proxySecret := new(corev1.Secret) proxySecret := new(corev1.Secret)
@@ -145,25 +150,25 @@ func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.R
// Find all EphemeralRunner with matching namespace and own by this EphemeralRunnerSet. // Find all EphemeralRunner with matching namespace and own by this EphemeralRunnerSet.
ephemeralRunnerList := new(v1alpha1.EphemeralRunnerList) ephemeralRunnerList := new(v1alpha1.EphemeralRunnerList)
err := r.List( if err := r.List(
ctx, ctx,
ephemeralRunnerList, ephemeralRunnerList,
client.InNamespace(req.Namespace), client.InNamespace(req.Namespace),
client.MatchingFields{resourceOwnerKey: req.Name}, client.MatchingFields{resourceOwnerKey: req.Name},
) ); err != nil {
if err != nil {
log.Error(err, "Unable to list child ephemeral runners") log.Error(err, "Unable to list child ephemeral runners")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
ephemeralRunnerState := newEphemeralRunnerState(ephemeralRunnerList) ephemeralRunnersByState := newEphemeralRunnersByStates(ephemeralRunnerList)
log.Info("Ephemeral runner counts", log.Info("Ephemeral runner counts",
"pending", len(ephemeralRunnerState.pending), "outdated", len(ephemeralRunnersByState.outdated),
"running", len(ephemeralRunnerState.running), "pending", len(ephemeralRunnersByState.pending),
"finished", len(ephemeralRunnerState.finished), "running", len(ephemeralRunnersByState.running),
"failed", len(ephemeralRunnerState.failed), "finished", len(ephemeralRunnersByState.finished),
"deleting", len(ephemeralRunnerState.deleting), "failed", len(ephemeralRunnersByState.failed),
"deleting", len(ephemeralRunnersByState.deleting),
) )
if r.PublishMetrics { if r.PublishMetrics {
@@ -183,16 +188,16 @@ func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.R
Organization: parsedURL.Organization, Organization: parsedURL.Organization,
Enterprise: parsedURL.Enterprise, Enterprise: parsedURL.Enterprise,
}, },
len(ephemeralRunnerState.pending), len(ephemeralRunnersByState.pending),
len(ephemeralRunnerState.running), len(ephemeralRunnersByState.running),
len(ephemeralRunnerState.failed), len(ephemeralRunnersByState.failed),
) )
} }
total := ephemeralRunnerState.scaleTotal() total := ephemeralRunnersByState.scaleTotal()
if ephemeralRunnerSet.Spec.PatchID == 0 || ephemeralRunnerSet.Spec.PatchID != ephemeralRunnerState.latestPatchID { if ephemeralRunnerSet.Spec.PatchID == 0 || ephemeralRunnerSet.Spec.PatchID != ephemeralRunnersByState.latestPatchID {
defer func() { defer func() {
if err := r.cleanupFinishedEphemeralRunners(ctx, ephemeralRunnerState.finished, log); err != nil { if err := r.cleanupFinishedEphemeralRunners(ctx, ephemeralRunnersByState.finished, log); err != nil {
log.Error(err, "failed to cleanup finished ephemeral runners") log.Error(err, "failed to cleanup finished ephemeral runners")
} }
}() }()
@@ -217,8 +222,8 @@ func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.R
if err := r.deleteIdleEphemeralRunners( if err := r.deleteIdleEphemeralRunners(
ctx, ctx,
ephemeralRunnerSet, ephemeralRunnerSet,
ephemeralRunnerState.pending, ephemeralRunnersByState.pending,
ephemeralRunnerState.running, ephemeralRunnersByState.running,
count, count,
log, log,
); err != nil { ); err != nil {
@@ -228,25 +233,41 @@ func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.R
} }
} }
return ctrl.Result{}, r.updateStatus(ctx, ephemeralRunnerSet, ephemeralRunnersByState, log)
}
func (r *EphemeralRunnerSetReconciler) updateStatus(ctx context.Context, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, state *ephemeralRunnersByState, log logr.Logger) error {
total := state.scaleTotal()
var phase v1alpha1.EphemeralRunnerSetPhase
switch {
case len(state.outdated) > 0:
phase = v1alpha1.EphemeralRunnerSetPhaseOutdated
case ephemeralRunnerSet.Status.Phase == "":
phase = v1alpha1.EphemeralRunnerSetPhaseRunning
default:
phase = ephemeralRunnerSet.Status.Phase
}
desiredStatus := v1alpha1.EphemeralRunnerSetStatus{ desiredStatus := v1alpha1.EphemeralRunnerSetStatus{
CurrentReplicas: total, CurrentReplicas: total,
PendingEphemeralRunners: len(ephemeralRunnerState.pending), Phase: phase,
RunningEphemeralRunners: len(ephemeralRunnerState.running), PendingEphemeralRunners: len(state.pending),
FailedEphemeralRunners: len(ephemeralRunnerState.failed), RunningEphemeralRunners: len(state.running),
FailedEphemeralRunners: len(state.failed),
} }
// Update the status if needed. // Update the status if needed.
if ephemeralRunnerSet.Status != desiredStatus { if ephemeralRunnerSet.Status != desiredStatus {
log.Info("Updating status with current runners count", "count", total) log.Info("Updating status with current runners count", "count", total)
ephemeralRunnerSet := ephemeralRunnerSet.DeepCopy()
ephemeralRunnerSet.Status.CurrentReplicas = -1 // ALWAYS update current replicas
if err := patchSubResource(ctx, r.Status(), ephemeralRunnerSet, func(obj *v1alpha1.EphemeralRunnerSet) { if err := patchSubResource(ctx, r.Status(), ephemeralRunnerSet, func(obj *v1alpha1.EphemeralRunnerSet) {
obj.Status = desiredStatus obj.Status = desiredStatus
}); err != nil { }); err != nil {
log.Error(err, "Failed to update status with current runners count") log.Error(err, "Failed to update status with current runners count")
return ctrl.Result{}, err return err
} }
} }
return nil
return ctrl.Result{}, nil
} }
func (r *EphemeralRunnerSetReconciler) cleanupFinishedEphemeralRunners(ctx context.Context, finishedEphemeralRunners []*v1alpha1.EphemeralRunner, log logr.Logger) error { func (r *EphemeralRunnerSetReconciler) cleanupFinishedEphemeralRunners(ctx context.Context, finishedEphemeralRunners []*v1alpha1.EphemeralRunner, log logr.Logger) error {
@@ -290,7 +311,6 @@ func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Conte
return false, fmt.Errorf("failed to list child ephemeral runners: %w", err) return false, fmt.Errorf("failed to list child ephemeral runners: %w", err)
} }
log.Info("Actual Ephemeral runner counts", "count", len(ephemeralRunnerList.Items))
// only if there are no ephemeral runners left, return true // only if there are no ephemeral runners left, return true
if len(ephemeralRunnerList.Items) == 0 { if len(ephemeralRunnerList.Items) == 0 {
err := r.cleanUpProxySecret(ctx, ephemeralRunnerSet, log) err := r.cleanUpProxySecret(ctx, ephemeralRunnerSet, log)
@@ -301,7 +321,7 @@ func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Conte
return true, nil return true, nil
} }
ephemeralRunnerState := newEphemeralRunnerState(ephemeralRunnerList) ephemeralRunnerState := newEphemeralRunnersByStates(ephemeralRunnerList)
log.Info("Clean up runner counts", log.Info("Clean up runner counts",
"pending", len(ephemeralRunnerState.pending), "pending", len(ephemeralRunnerState.pending),
@@ -309,11 +329,12 @@ func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Conte
"finished", len(ephemeralRunnerState.finished), "finished", len(ephemeralRunnerState.finished),
"failed", len(ephemeralRunnerState.failed), "failed", len(ephemeralRunnerState.failed),
"deleting", len(ephemeralRunnerState.deleting), "deleting", len(ephemeralRunnerState.deleting),
"outdated", len(ephemeralRunnerState.outdated),
) )
log.Info("Cleanup finished or failed ephemeral runners") log.Info("Cleanup terminated ephemeral runners")
var errs []error var errs []error
for _, ephemeralRunner := range append(ephemeralRunnerState.finished, ephemeralRunnerState.failed...) { for _, ephemeralRunner := range ephemeralRunnerState.terminated() {
log.Info("Deleting ephemeral runner", "name", ephemeralRunner.Name) log.Info("Deleting ephemeral runner", "name", ephemeralRunner.Name)
if err := r.Delete(ctx, ephemeralRunner); err != nil && !kerrors.IsNotFound(err) { if err := r.Delete(ctx, ephemeralRunner); err != nil && !kerrors.IsNotFound(err) {
errs = append(errs, err) errs = append(errs, err)
@@ -359,7 +380,7 @@ func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Conte
func (r *EphemeralRunnerSetReconciler) createEphemeralRunners(ctx context.Context, runnerSet *v1alpha1.EphemeralRunnerSet, count int, log logr.Logger) error { func (r *EphemeralRunnerSetReconciler) createEphemeralRunners(ctx context.Context, runnerSet *v1alpha1.EphemeralRunnerSet, count int, log logr.Logger) error {
// Track multiple errors at once and return the bundle. // Track multiple errors at once and return the bundle.
errs := make([]error, 0) errs := make([]error, 0)
for i := 0; i < count; i++ { for i := range count {
ephemeralRunner := r.newEphemeralRunner(runnerSet) ephemeralRunner := r.newEphemeralRunner(runnerSet)
if runnerSet.Spec.EphemeralRunnerSpec.Proxy != nil { if runnerSet.Spec.EphemeralRunnerSpec.Proxy != nil {
ephemeralRunner.Spec.ProxySecretRef = proxyEphemeralRunnerSetSecretName(runnerSet) ephemeralRunner.Spec.ProxySecretRef = proxyEphemeralRunnerSetSecretName(runnerSet)
@@ -448,7 +469,7 @@ func (r *EphemeralRunnerSetReconciler) deleteIdleEphemeralRunners(ctx context.Co
for runners.next() { for runners.next() {
ephemeralRunner := runners.object() ephemeralRunner := runners.object()
isDone := ephemeralRunner.IsDone() isDone := ephemeralRunner.IsDone()
if !isDone && ephemeralRunner.Status.RunnerId == 0 { if !isDone && ephemeralRunner.Status.RunnerID == 0 {
log.Info("Skipping ephemeral runner since it is not registered yet", "name", ephemeralRunner.Name) log.Info("Skipping ephemeral runner since it is not registered yet", "name", ephemeralRunner.Name)
continue continue
} }
@@ -457,7 +478,7 @@ func (r *EphemeralRunnerSetReconciler) deleteIdleEphemeralRunners(ctx context.Co
log.Info( log.Info(
"Skipping ephemeral runner since it is running a job", "Skipping ephemeral runner since it is running a job",
"name", ephemeralRunner.Name, "name", ephemeralRunner.Name,
"workflowRunId", ephemeralRunner.Status.WorkflowRunId, "workflowRunId", ephemeralRunner.Status.WorkflowRunID,
"jobId", ephemeralRunner.Status.JobID, "jobId", ephemeralRunner.Status.JobID,
) )
continue continue
@@ -482,21 +503,21 @@ func (r *EphemeralRunnerSetReconciler) deleteIdleEphemeralRunners(ctx context.Co
} }
func (r *EphemeralRunnerSetReconciler) deleteEphemeralRunnerWithActionsClient(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, actionsClient multiclient.Client, log logr.Logger) (bool, error) { func (r *EphemeralRunnerSetReconciler) deleteEphemeralRunnerWithActionsClient(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, actionsClient multiclient.Client, log logr.Logger) (bool, error) {
if err := actionsClient.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerId)); err != nil { if err := actionsClient.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerID)); err != nil {
if errors.Is(err, scaleset.JobStillRunningError) { if errors.Is(err, scaleset.JobStillRunningError) {
log.Info("Runner is still running a job, skipping deletion", "name", ephemeralRunner.Name, "runnerId", ephemeralRunner.Status.RunnerId) log.Info("Runner is still running a job, skipping deletion", "name", ephemeralRunner.Name, "runnerId", ephemeralRunner.Status.RunnerID)
return false, nil return false, nil
} }
return false, err return false, err
} }
log.Info("Deleting ephemeral runner after removing from the service", "name", ephemeralRunner.Name, "runnerId", ephemeralRunner.Status.RunnerId) log.Info("Deleting ephemeral runner after removing from the service", "name", ephemeralRunner.Name, "runnerId", ephemeralRunner.Status.RunnerID)
if err := r.Delete(ctx, ephemeralRunner); err != nil && !kerrors.IsNotFound(err) { if err := r.Delete(ctx, ephemeralRunner); err != nil && !kerrors.IsNotFound(err) {
return false, err return false, err
} }
log.Info("Deleted ephemeral runner", "name", ephemeralRunner.Name, "runnerId", ephemeralRunner.Status.RunnerId) log.Info("Deleted ephemeral runner", "name", ephemeralRunner.Name, "runnerId", ephemeralRunner.Status.RunnerID)
return true, nil return true, nil
} }
@@ -553,18 +574,19 @@ func (s *ephemeralRunnerStepper) len() int {
return len(s.items) return len(s.items)
} }
type ephemeralRunnerState struct { type ephemeralRunnersByState struct {
pending []*v1alpha1.EphemeralRunner pending []*v1alpha1.EphemeralRunner
running []*v1alpha1.EphemeralRunner running []*v1alpha1.EphemeralRunner
finished []*v1alpha1.EphemeralRunner finished []*v1alpha1.EphemeralRunner
failed []*v1alpha1.EphemeralRunner failed []*v1alpha1.EphemeralRunner
deleting []*v1alpha1.EphemeralRunner deleting []*v1alpha1.EphemeralRunner
outdated []*v1alpha1.EphemeralRunner
latestPatchID int latestPatchID int
} }
func newEphemeralRunnerState(ephemeralRunnerList *v1alpha1.EphemeralRunnerList) *ephemeralRunnerState { func newEphemeralRunnersByStates(ephemeralRunnerList *v1alpha1.EphemeralRunnerList) *ephemeralRunnersByState {
var ephemeralRunnerState ephemeralRunnerState var ephemeralRunnerState ephemeralRunnersByState
for i := range ephemeralRunnerList.Items { for i := range ephemeralRunnerList.Items {
r := &ephemeralRunnerList.Items[i] r := &ephemeralRunnerList.Items[i]
@@ -578,12 +600,14 @@ func newEphemeralRunnerState(ephemeralRunnerList *v1alpha1.EphemeralRunnerList)
} }
switch r.Status.Phase { switch r.Status.Phase {
case corev1.PodRunning: case v1alpha1.EphemeralRunnerPhaseRunning:
ephemeralRunnerState.running = append(ephemeralRunnerState.running, r) ephemeralRunnerState.running = append(ephemeralRunnerState.running, r)
case corev1.PodSucceeded: case v1alpha1.EphemeralRunnerPhaseSucceeded:
ephemeralRunnerState.finished = append(ephemeralRunnerState.finished, r) ephemeralRunnerState.finished = append(ephemeralRunnerState.finished, r)
case corev1.PodFailed: case v1alpha1.EphemeralRunnerPhaseFailed:
ephemeralRunnerState.failed = append(ephemeralRunnerState.failed, r) ephemeralRunnerState.failed = append(ephemeralRunnerState.failed, r)
case v1alpha1.EphemeralRunnerPhaseOutdated:
ephemeralRunnerState.outdated = append(ephemeralRunnerState.outdated, r)
default: default:
// Pending or no phase should be considered as pending. // Pending or no phase should be considered as pending.
// //
@@ -595,6 +619,10 @@ func newEphemeralRunnerState(ephemeralRunnerList *v1alpha1.EphemeralRunnerList)
return &ephemeralRunnerState return &ephemeralRunnerState
} }
func (s *ephemeralRunnerState) scaleTotal() int { func (s *ephemeralRunnersByState) terminated() []*v1alpha1.EphemeralRunner {
return append(s.finished, append(s.failed, s.outdated...)...)
}
func (s *ephemeralRunnersByState) scaleTotal() int {
return len(s.pending) + len(s.running) + len(s.failed) return len(s.pending) + len(s.running) + len(s.failed)
} }
@@ -159,10 +159,10 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
// Set status to simulate a configured EphemeralRunner // Set status to simulate a configured EphemeralRunner
refetch := false refetch := false
for i, runner := range runnerList.Items { for i, runner := range runnerList.Items {
if runner.Status.RunnerId == 0 { if runner.Status.RunnerID == 0 {
updatedRunner := runner.DeepCopy() updatedRunner := runner.DeepCopy()
updatedRunner.Status.Phase = corev1.PodRunning updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
updatedRunner.Status.RunnerId = i + 100 updatedRunner.Status.RunnerID = i + 100
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner)) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
refetch = true refetch = true
@@ -219,10 +219,10 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
// Set status to simulate a configured EphemeralRunner // Set status to simulate a configured EphemeralRunner
refetch := false refetch := false
for i, runner := range runnerList.Items { for i, runner := range runnerList.Items {
if runner.Status.RunnerId == 0 { if runner.Status.RunnerID == 0 {
updatedRunner := runner.DeepCopy() updatedRunner := runner.DeepCopy()
updatedRunner.Status.Phase = corev1.PodRunning updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
updatedRunner.Status.RunnerId = i + 100 updatedRunner.Status.RunnerID = i + 100
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner)) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
refetch = true refetch = true
@@ -382,12 +382,12 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created") ).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
updatedRunner := runnerList.Items[0].DeepCopy() updatedRunner := runnerList.Items[0].DeepCopy()
updatedRunner.Status.Phase = corev1.PodSucceeded updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
updatedRunner = runnerList.Items[1].DeepCopy() updatedRunner = runnerList.Items[1].DeepCopy()
updatedRunner.Status.Phase = corev1.PodRunning updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
@@ -460,12 +460,12 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created") ).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
updatedRunner := runnerList.Items[0].DeepCopy() updatedRunner := runnerList.Items[0].DeepCopy()
updatedRunner.Status.Phase = corev1.PodSucceeded updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
updatedRunner = runnerList.Items[1].DeepCopy() updatedRunner = runnerList.Items[1].DeepCopy()
updatedRunner.Status.Phase = corev1.PodPending updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhasePending
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
@@ -512,12 +512,12 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created") ).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
updatedRunner := runnerList.Items[0].DeepCopy() updatedRunner := runnerList.Items[0].DeepCopy()
updatedRunner.Status.Phase = corev1.PodSucceeded updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
updatedRunner = runnerList.Items[1].DeepCopy() updatedRunner = runnerList.Items[1].DeepCopy()
updatedRunner.Status.Phase = corev1.PodRunning updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
@@ -547,7 +547,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
} }
for _, runner := range runnerList.Items { for _, runner := range runnerList.Items {
if runner.Status.Phase == corev1.PodSucceeded { if runner.Status.Phase == v1alpha1.EphemeralRunnerPhaseSucceeded {
return fmt.Errorf("Runner %s is in Succeeded phase", runner.Name) return fmt.Errorf("Runner %s is in Succeeded phase", runner.Name)
} }
} }
@@ -586,12 +586,12 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created") ).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
updatedRunner := runnerList.Items[0].DeepCopy() updatedRunner := runnerList.Items[0].DeepCopy()
updatedRunner.Status.Phase = corev1.PodSucceeded updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
updatedRunner = runnerList.Items[1].DeepCopy() updatedRunner = runnerList.Items[1].DeepCopy()
updatedRunner.Status.Phase = corev1.PodPending updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhasePending
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
@@ -607,9 +607,9 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
succeeded := 0 succeeded := 0
for _, runner := range runnerList.Items { for _, runner := range runnerList.Items {
switch runner.Status.Phase { switch runner.Status.Phase {
case corev1.PodSucceeded: case v1alpha1.EphemeralRunnerPhaseSucceeded:
succeeded++ succeeded++
case corev1.PodPending: case v1alpha1.EphemeralRunnerPhasePending:
pending++ pending++
} }
} }
@@ -649,7 +649,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
return fmt.Errorf("Expected 1 runner, got %d", len(runnerList.Items)) return fmt.Errorf("Expected 1 runner, got %d", len(runnerList.Items))
} }
if runnerList.Items[0].Status.Phase != corev1.PodPending { if runnerList.Items[0].Status.Phase != v1alpha1.EphemeralRunnerPhasePending {
return fmt.Errorf("Expected runner to be in Pending, got %s", runnerList.Items[0].Status.Phase) return fmt.Errorf("Expected runner to be in Pending, got %s", runnerList.Items[0].Status.Phase)
} }
@@ -661,7 +661,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
// Now, the ephemeral runner finally is done and we can scale down to 0 // Now, the ephemeral runner finally is done and we can scale down to 0
updatedRunner = runnerList.Items[0].DeepCopy() updatedRunner = runnerList.Items[0].DeepCopy()
updatedRunner.Status.Phase = corev1.PodSucceeded updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
@@ -707,12 +707,12 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
// Put one runner in Pending and one in Running // Put one runner in Pending and one in Running
updatedRunner := runnerList.Items[0].DeepCopy() updatedRunner := runnerList.Items[0].DeepCopy()
updatedRunner.Status.Phase = corev1.PodPending updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhasePending
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
updatedRunner = runnerList.Items[1].DeepCopy() updatedRunner = runnerList.Items[1].DeepCopy()
updatedRunner.Status.Phase = corev1.PodRunning updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
@@ -730,9 +730,9 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
for _, runner := range runnerList.Items { for _, runner := range runnerList.Items {
switch runner.Status.Phase { switch runner.Status.Phase {
case corev1.PodPending: case v1alpha1.EphemeralRunnerPhasePending:
pending++ pending++
case corev1.PodRunning: case v1alpha1.EphemeralRunnerPhaseRunning:
running++ running++
} }
@@ -777,12 +777,12 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
// Now, let's say ephemeral runner controller patched these ephemeral runners with the registration. // Now, let's say ephemeral runner controller patched these ephemeral runners with the registration.
updatedRunner = runnerList.Items[0].DeepCopy() updatedRunner = runnerList.Items[0].DeepCopy()
updatedRunner.Status.RunnerId = 1 updatedRunner.Status.RunnerID = 1
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
updatedRunner = runnerList.Items[1].DeepCopy() updatedRunner = runnerList.Items[1].DeepCopy()
updatedRunner.Status.RunnerId = 2 updatedRunner.Status.RunnerID = 2
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
@@ -830,12 +830,12 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
// Put one runner in Succeeded and one in Running // Put one runner in Succeeded and one in Running
updatedRunner := runnerList.Items[0].DeepCopy() updatedRunner := runnerList.Items[0].DeepCopy()
updatedRunner.Status.Phase = corev1.PodSucceeded updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
updatedRunner = runnerList.Items[1].DeepCopy() updatedRunner = runnerList.Items[1].DeepCopy()
updatedRunner.Status.Phase = corev1.PodRunning updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1])) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
@@ -854,9 +854,9 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
for _, runner := range runnerList.Items { for _, runner := range runnerList.Items {
switch runner.Status.Phase { switch runner.Status.Phase {
case corev1.PodSucceeded: case v1alpha1.EphemeralRunnerPhaseSucceeded:
succeeded++ succeeded++
case corev1.PodRunning: case v1alpha1.EphemeralRunnerPhaseRunning:
running++ running++
} }
} }
@@ -898,7 +898,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
} }
for _, runner := range runnerList.Items { for _, runner := range runnerList.Items {
if runner.Status.Phase == corev1.PodSucceeded { if runner.Status.Phase == v1alpha1.EphemeralRunnerPhaseSucceeded {
return fmt.Errorf("Expected no runners in Succeeded phase, got one") return fmt.Errorf("Expected no runners in Succeeded phase, got one")
} }
} }
@@ -943,7 +943,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
var failedOriginal *v1alpha1.EphemeralRunner var failedOriginal *v1alpha1.EphemeralRunner
var empty []*v1alpha1.EphemeralRunner var empty []*v1alpha1.EphemeralRunner
for _, runner := range runnerList.Items { for _, runner := range runnerList.Items {
switch runner.Status.RunnerId { switch runner.Status.RunnerID {
case 101: case 101:
pendingOriginal = runner.DeepCopy() pendingOriginal = runner.DeepCopy()
case 102: case 102:
@@ -962,8 +962,8 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
empty = empty[1:] empty = empty[1:]
pending := pendingOriginal.DeepCopy() pending := pendingOriginal.DeepCopy()
pending.Status.RunnerId = 101 pending.Status.RunnerID = 101
pending.Status.Phase = corev1.PodPending pending.Status.Phase = v1alpha1.EphemeralRunnerPhasePending
err = k8sClient.Status().Patch(ctx, pending, client.MergeFrom(pendingOriginal)) err = k8sClient.Status().Patch(ctx, pending, client.MergeFrom(pendingOriginal))
if err != nil { if err != nil {
@@ -976,8 +976,8 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
runningOriginal = empty[0] runningOriginal = empty[0]
empty = empty[1:] empty = empty[1:]
running := runningOriginal.DeepCopy() running := runningOriginal.DeepCopy()
running.Status.RunnerId = 102 running.Status.RunnerID = 102
running.Status.Phase = corev1.PodRunning running.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
err = k8sClient.Status().Patch(ctx, running, client.MergeFrom(runningOriginal)) err = k8sClient.Status().Patch(ctx, running, client.MergeFrom(runningOriginal))
if err != nil { if err != nil {
@@ -990,8 +990,8 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
failedOriginal = empty[0] failedOriginal = empty[0]
failed := pendingOriginal.DeepCopy() failed := pendingOriginal.DeepCopy()
failed.Status.RunnerId = 103 failed.Status.RunnerID = 103
failed.Status.Phase = corev1.PodFailed failed.Status.Phase = v1alpha1.EphemeralRunnerPhaseFailed
err = k8sClient.Status().Patch(ctx, failed, client.MergeFrom(failedOriginal)) err = k8sClient.Status().Patch(ctx, failed, client.MergeFrom(failedOriginal))
if err != nil { if err != nil {
@@ -1006,6 +1006,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
).Should(BeTrue(), "Failed to eventually update to one pending, one running and one failed") ).Should(BeTrue(), "Failed to eventually update to one pending, one running and one failed")
desiredStatus := v1alpha1.EphemeralRunnerSetStatus{ desiredStatus := v1alpha1.EphemeralRunnerSetStatus{
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
CurrentReplicas: 3, CurrentReplicas: 3,
PendingEphemeralRunners: 1, PendingEphemeralRunners: 1,
RunningEphemeralRunners: 1, RunningEphemeralRunners: 1,
@@ -1052,6 +1053,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
PendingEphemeralRunners: 0, PendingEphemeralRunners: 0,
RunningEphemeralRunners: 0, RunningEphemeralRunners: 0,
FailedEphemeralRunners: 1, FailedEphemeralRunners: 1,
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
} }
Eventually( Eventually(
@@ -1070,7 +1072,13 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
err = k8sClient.Delete(ctx, &runnerList.Items[0]) err = k8sClient.Delete(ctx, &runnerList.Items[0])
Expect(err).To(BeNil(), "Failed to delete failed ephemeral runner") Expect(err).To(BeNil(), "Failed to delete failed ephemeral runner")
desiredStatus = v1alpha1.EphemeralRunnerSetStatus{} // empty desiredStatus = v1alpha1.EphemeralRunnerSetStatus{
CurrentReplicas: 0,
PendingEphemeralRunners: 0,
RunningEphemeralRunners: 0,
FailedEphemeralRunners: 0,
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
}
Eventually( Eventually(
func() (v1alpha1.EphemeralRunnerSetStatus, error) { func() (v1alpha1.EphemeralRunnerSetStatus, error) {
updated := new(v1alpha1.EphemeralRunnerSet) updated := new(v1alpha1.EphemeralRunnerSet)
@@ -1222,10 +1230,10 @@ var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func(
// Set status to simulate a configured EphemeralRunner // Set status to simulate a configured EphemeralRunner
refetch := false refetch := false
for i, runner := range runnerList.Items { for i, runner := range runnerList.Items {
if runner.Status.RunnerId == 0 { if runner.Status.RunnerID == 0 {
updatedRunner := runner.DeepCopy() updatedRunner := runner.DeepCopy()
updatedRunner.Status.Phase = corev1.PodSucceeded updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
updatedRunner.Status.RunnerId = i + 100 updatedRunner.Status.RunnerID = i + 100
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner)) err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner") Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
refetch = true refetch = true
@@ -1355,8 +1363,8 @@ var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func(
).Should(BeEquivalentTo(1), "failed to create ephemeral runner") ).Should(BeEquivalentTo(1), "failed to create ephemeral runner")
runner := runnerList.Items[0].DeepCopy() runner := runnerList.Items[0].DeepCopy()
runner.Status.Phase = corev1.PodRunning runner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
runner.Status.RunnerId = 100 runner.Status.RunnerID = 100
err = k8sClient.Status().Patch(ctx, runner, client.MergeFrom(&runnerList.Items[0])) err = k8sClient.Status().Patch(ctx, runner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update ephemeral runner status") Expect(err).NotTo(HaveOccurred(), "failed to update ephemeral runner status")
@@ -716,6 +716,10 @@ func (b *ResourceBuilder) newEphemeralRunnerPod(runner *v1alpha1.EphemeralRunner
Name: EnvVarRunnerExtraUserAgent, Name: EnvVarRunnerExtraUserAgent,
Value: fmt.Sprintf("actions-runner-controller/%s", build.Version), Value: fmt.Sprintf("actions-runner-controller/%s", build.Version),
}, },
corev1.EnvVar{
Name: EnvVarRunnerDeprecatedExitCode,
Value: "1",
},
) )
c.Env = append(c.Env, envs...) c.Env = append(c.Env, envs...)
} }