Use metrics to display runner statuses instead of status field for EphemeralRunnerSet and AutoscalingRunnerSet (#4557)

This commit is contained in:
Nikola Jokic 2026-07-14 19:31:11 +02:00 committed by GitHub
parent 368e2f28b8
commit f6a3d738de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 509 additions and 449 deletions

View File

@ -42,11 +42,11 @@ type AutoscalingListenerSpec struct {
// +optional // +optional
EphemeralRunnerSetName string `json:"ephemeralRunnerSetName,omitempty"` EphemeralRunnerSetName string `json:"ephemeralRunnerSetName,omitempty"`
// +kubebuilder:validation:Minimum:=0 // +kubebuilder:validation:Minimum=0
// +optional // +optional
MaxRunners int `json:"maxRunners,omitempty"` MaxRunners int `json:"maxRunners,omitempty"`
// +kubebuilder:validation:Minimum:=0 // +kubebuilder:validation:Minimum=0
// +optional // +optional
MinRunners int `json:"minRunners,omitempty"` MinRunners int `json:"minRunners,omitempty"`
@ -109,6 +109,7 @@ type AutoscalingListener struct {
Status AutoscalingListenerStatus `json:"status,omitempty"` Status AutoscalingListenerStatus `json:"status,omitempty"`
} }
// AutoscalingListenerList is a list of AutoscalingListener resources
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
// AutoscalingListenerList contains a list of AutoscalingListener // AutoscalingListenerList contains a list of AutoscalingListener
type AutoscalingListenerList struct { type AutoscalingListenerList struct {

View File

@ -317,20 +317,8 @@ type HistogramMetric struct {
// AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet // AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet
type AutoscalingRunnerSetStatus struct { type AutoscalingRunnerSetStatus struct {
// +optional
CurrentRunners int `json:"currentRunners"`
// +optional // +optional
Phase AutoscalingRunnerSetPhase `json:"phase"` Phase AutoscalingRunnerSetPhase `json:"phase"`
// EphemeralRunner counts separated by the stage ephemeral runners are in, taken from the EphemeralRunnerSet
// +optional
PendingEphemeralRunners int `json:"pendingEphemeralRunners"`
// +optional
RunningEphemeralRunners int `json:"runningEphemeralRunners"`
// +optional
FailedEphemeralRunners int `json:"failedEphemeralRunners"`
} }
type AutoscalingRunnerSetPhase string type AutoscalingRunnerSetPhase string

View File

@ -40,15 +40,6 @@ type EphemeralRunnerSetSpec struct {
// EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet // EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet
type EphemeralRunnerSetStatus struct { type EphemeralRunnerSetStatus struct {
// CurrentReplicas is the number of currently running EphemeralRunner resources being managed by this EphemeralRunnerSet.
// +optional
CurrentReplicas int `json:"currentReplicas"`
// +optional
PendingEphemeralRunners int `json:"pendingEphemeralRunners"`
// +optional
RunningEphemeralRunners int `json:"runningEphemeralRunners"`
// +optional
FailedEphemeralRunners int `json:"failedEphemeralRunners"`
// +optional // +optional
Phase EphemeralRunnerSetPhase `json:"phase"` Phase EphemeralRunnerSetPhase `json:"phase"`
} }
@ -66,11 +57,6 @@ const (
// +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"
// +kubebuilder:printcolumn:JSONPath=".status.currentReplicas", name="CurrentReplicas",type="integer"
// +kubebuilder:printcolumn:JSONPath=".status.pendingEphemeralRunners",name=Pending Runners,type=integer
// +kubebuilder:printcolumn:JSONPath=".status.runningEphemeralRunners",name=Running Runners,type=integer
// +kubebuilder:printcolumn:JSONPath=".status.finishedEphemeralRunners",name=Finished Runners,type=integer
// +kubebuilder:printcolumn:JSONPath=".status.deletingEphemeralRunners",name=Deleting Runners,type=integer
// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Phase,type=string // +kubebuilder:printcolumn:JSONPath=".status.phase",name=Phase,type=string
// EphemeralRunnerSet is the Schema for the ephemeralrunnersets API // EphemeralRunnerSet is the Schema for the ephemeralrunnersets API

View File

@ -16541,16 +16541,8 @@ spec:
status: status:
description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet
properties: properties:
currentRunners:
type: integer
failedEphemeralRunners:
type: integer
pendingEphemeralRunners:
type: integer
phase: phase:
type: string type: string
runningEphemeralRunners:
type: integer
type: object type: object
type: object type: object
served: true served: true

View File

@ -18,21 +18,6 @@ spec:
- jsonPath: .spec.replicas - jsonPath: .spec.replicas
name: DesiredReplicas name: DesiredReplicas
type: integer type: integer
- jsonPath: .status.currentReplicas
name: CurrentReplicas
type: integer
- jsonPath: .status.pendingEphemeralRunners
name: Pending Runners
type: integer
- jsonPath: .status.runningEphemeralRunners
name: Running Runners
type: integer
- jsonPath: .status.finishedEphemeralRunners
name: Finished Runners
type: integer
- jsonPath: .status.deletingEphemeralRunners
name: Deleting Runners
type: integer
- jsonPath: .status.phase - jsonPath: .status.phase
name: Phase name: Phase
type: string type: string
@ -8310,18 +8295,9 @@ spec:
status: status:
description: EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet description: EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet
properties: properties:
currentReplicas:
description: CurrentReplicas is the number of currently running EphemeralRunner resources being managed by this EphemeralRunnerSet.
type: integer
failedEphemeralRunners:
type: integer
pendingEphemeralRunners:
type: integer
phase: phase:
description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource
type: string type: string
runningEphemeralRunners:
type: integer
type: object type: object
type: object type: object
served: true served: true

View File

@ -16541,16 +16541,8 @@ spec:
status: status:
description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet
properties: properties:
currentRunners:
type: integer
failedEphemeralRunners:
type: integer
pendingEphemeralRunners:
type: integer
phase: phase:
type: string type: string
runningEphemeralRunners:
type: integer
type: object type: object
type: object type: object
served: true served: true

View File

@ -18,21 +18,6 @@ spec:
- jsonPath: .spec.replicas - jsonPath: .spec.replicas
name: DesiredReplicas name: DesiredReplicas
type: integer type: integer
- jsonPath: .status.currentReplicas
name: CurrentReplicas
type: integer
- jsonPath: .status.pendingEphemeralRunners
name: Pending Runners
type: integer
- jsonPath: .status.runningEphemeralRunners
name: Running Runners
type: integer
- jsonPath: .status.finishedEphemeralRunners
name: Finished Runners
type: integer
- jsonPath: .status.deletingEphemeralRunners
name: Deleting Runners
type: integer
- jsonPath: .status.phase - jsonPath: .status.phase
name: Phase name: Phase
type: string type: string
@ -8310,18 +8295,9 @@ spec:
status: status:
description: EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet description: EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet
properties: properties:
currentReplicas:
description: CurrentReplicas is the number of currently running EphemeralRunner resources being managed by this EphemeralRunnerSet.
type: integer
failedEphemeralRunners:
type: integer
pendingEphemeralRunners:
type: integer
phase: phase:
description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource
type: string type: string
runningEphemeralRunners:
type: integer
type: object type: object
type: object type: object
served: true served: true

View File

@ -16541,16 +16541,8 @@ spec:
status: status:
description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet
properties: properties:
currentRunners:
type: integer
failedEphemeralRunners:
type: integer
pendingEphemeralRunners:
type: integer
phase: phase:
type: string type: string
runningEphemeralRunners:
type: integer
type: object type: object
type: object type: object
served: true served: true

View File

@ -18,21 +18,6 @@ spec:
- jsonPath: .spec.replicas - jsonPath: .spec.replicas
name: DesiredReplicas name: DesiredReplicas
type: integer type: integer
- jsonPath: .status.currentReplicas
name: CurrentReplicas
type: integer
- jsonPath: .status.pendingEphemeralRunners
name: Pending Runners
type: integer
- jsonPath: .status.runningEphemeralRunners
name: Running Runners
type: integer
- jsonPath: .status.finishedEphemeralRunners
name: Finished Runners
type: integer
- jsonPath: .status.deletingEphemeralRunners
name: Deleting Runners
type: integer
- jsonPath: .status.phase - jsonPath: .status.phase
name: Phase name: Phase
type: string type: string
@ -8310,18 +8295,9 @@ spec:
status: status:
description: EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet description: EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet
properties: properties:
currentReplicas:
description: CurrentReplicas is the number of currently running EphemeralRunner resources being managed by this EphemeralRunnerSet.
type: integer
failedEphemeralRunners:
type: integer
pendingEphemeralRunners:
type: integer
phase: phase:
description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource description: EphemeralRunnerSetPhase is the phase of the ephemeral runner set resource
type: string type: string
runningEphemeralRunners:
type: integer
type: object type: object
type: object type: object
served: true served: true

View File

@ -292,18 +292,6 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
} }
if ephemeralRunnerSet.Annotations[annotationKeyIntegrityHash] != desired.Annotations[annotationKeyIntegrityHash] { if ephemeralRunnerSet.Annotations[annotationKeyIntegrityHash] != desired.Annotations[annotationKeyIntegrityHash] {
// When runners are actively processing jobs, defer the spec update:
// delete the listener to stop accepting new jobs, but leave the ERS
// (and its running pods) untouched until all jobs have drained.
if ephemeralRunnerSet.Status.RunningEphemeralRunners+ephemeralRunnerSet.Status.PendingEphemeralRunners > 0 {
log.Info("Ephemeral runner set spec changed but runners are still active; deleting listener to stop new jobs")
if _, err := r.cleanupListener(ctx, &autoscalingRunnerSet, log); err != nil {
log.Error(err, "Failed to clean up listener while waiting for runners to drain")
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 1 * time.Second}, nil
}
original := ephemeralRunnerSet.DeepCopy() original := ephemeralRunnerSet.DeepCopy()
ephemeralRunnerSet.Spec.EphemeralRunnerMetadata = desired.Spec.EphemeralRunnerMetadata ephemeralRunnerSet.Spec.EphemeralRunnerMetadata = desired.Spec.EphemeralRunnerMetadata
ephemeralRunnerSet.Spec.EphemeralRunnerSpec = desired.Spec.EphemeralRunnerSpec ephemeralRunnerSet.Spec.EphemeralRunnerSpec = desired.Spec.EphemeralRunnerSpec
@ -386,7 +374,6 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
if err := r.updateStatus( if err := r.updateStatus(
ctx, ctx,
&autoscalingRunnerSet, &autoscalingRunnerSet,
&ephemeralRunnerSet,
v1alpha1.AutoscalingRunnerSetPhaseRunning, v1alpha1.AutoscalingRunnerSetPhaseRunning,
log, log,
); err != nil { ); err != nil {
@ -432,24 +419,14 @@ func (r *AutoscalingRunnerSetReconciler) cleanUpResources(ctx context.Context, a
} }
// Update the status of autoscaling runner set if necessary // 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 { func (r *AutoscalingRunnerSetReconciler) updateStatus(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, phase v1alpha1.AutoscalingRunnerSetPhase, log logr.Logger) error {
countDiff := ephemeralRunnerSet != nil && ephemeralRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners
phaseDiff := phase != autoscalingRunnerSet.Status.Phase phaseDiff := phase != autoscalingRunnerSet.Status.Phase
if !countDiff && !phaseDiff { if !phaseDiff {
return nil return nil
} }
original := autoscalingRunnerSet.DeepCopy() original := autoscalingRunnerSet.DeepCopy()
if phaseDiff { autoscalingRunnerSet.Status.Phase = phase
autoscalingRunnerSet.Status.Phase = phase
}
if countDiff && ephemeralRunnerSet != nil {
autoscalingRunnerSet.Status.CurrentRunners = ephemeralRunnerSet.Status.CurrentReplicas
autoscalingRunnerSet.Status.PendingEphemeralRunners = ephemeralRunnerSet.Status.PendingEphemeralRunners
autoscalingRunnerSet.Status.RunningEphemeralRunners = ephemeralRunnerSet.Status.RunningEphemeralRunners
autoscalingRunnerSet.Status.FailedEphemeralRunners = ephemeralRunnerSet.Status.FailedEphemeralRunners
}
if err := r.Status().Patch(ctx, autoscalingRunnerSet, client.MergeFrom(original)); err != nil { if err := r.Status().Patch(ctx, autoscalingRunnerSet, client.MergeFrom(original)); err != nil {
log.Error(err, "Failed to patch autoscaling runner set status") log.Error(err, "Failed to patch autoscaling runner set status")

View File

@ -837,111 +837,6 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
}) })
}) })
Context("When updating an AutoscalingRunnerSet with running or pending jobs", func() {
It("It should wait for running and pending jobs to finish before applying the update.", func() {
// Wait till the listener is created
listener := new(v1alpha1.AutoscalingListener)
Eventually(
func() error {
return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).Should(Succeed(), "Listener should be created")
// Wait till the ephemeral runner set is created
Eventually(
func() (int, error) {
runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
if err != nil {
return 0, err
}
return len(runnerSetList.Items), nil
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).Should(BeEquivalentTo(1), "Only one EphemeralRunnerSet should be created")
runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
// Emulate running and pending jobs
runnerSet := runnerSetList.Items[0]
activeRunnerSet := runnerSet.DeepCopy()
activeRunnerSet.Status.CurrentReplicas = 6
activeRunnerSet.Status.FailedEphemeralRunners = 1
activeRunnerSet.Status.RunningEphemeralRunners = 2
activeRunnerSet.Status.PendingEphemeralRunners = 3
desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{
CurrentRunners: activeRunnerSet.Status.CurrentReplicas,
Phase: v1alpha1.AutoscalingRunnerSetPhaseRunning,
PendingEphemeralRunners: activeRunnerSet.Status.PendingEphemeralRunners,
RunningEphemeralRunners: activeRunnerSet.Status.RunningEphemeralRunners,
FailedEphemeralRunners: activeRunnerSet.Status.FailedEphemeralRunners,
}
err = k8sClient.Status().Patch(ctx, activeRunnerSet, client.MergeFrom(&runnerSet))
Expect(err).NotTo(HaveOccurred(), "Failed to patch runner set status")
Eventually(
func() (v1alpha1.AutoscalingRunnerSetStatus, error) {
updated := new(v1alpha1.AutoscalingRunnerSet)
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, updated)
if err != nil {
return v1alpha1.AutoscalingRunnerSetStatus{}, fmt.Errorf("failed to get AutoScalingRunnerSet: %w", err)
}
return updated.Status, nil
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).Should(BeEquivalentTo(desiredStatus), "AutoScalingRunnerSet status should be updated")
// Patch the AutoScalingRunnerSet image which should trigger
// the recreation of the Listener and EphemeralRunnerSet
patched := autoscalingRunnerSet.DeepCopy()
if patched.Annotations == nil {
patched.Annotations = make(map[string]string)
}
patched.Annotations[annotationKeyIntegrityHash] = "testgroup2"
patched.Spec.Template.Spec = corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runner",
Image: "ghcr.io/actions/abcd:1.1.1",
},
},
}
err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")
autoscalingRunnerSet = patched.DeepCopy()
// The EphemeralRunnerSet should not be recreated
Consistently(
func() (string, error) {
runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
Expect(err).NotTo(HaveOccurred(), "failed to fetch AutoScalingRunnerSet")
return runnerSetList.Items[0].Name, nil
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).Should(Equal(activeRunnerSet.Name), "The EphemeralRunnerSet should not be recreated")
// The listener should not be recreated
Consistently(
func() error {
return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).ShouldNot(Succeed(), "Listener should not be recreated")
})
})
It("Should update Status on EphemeralRunnerSet status Update", func() { It("Should update Status on EphemeralRunnerSet status Update", func() {
ars := new(v1alpha1.AutoscalingRunnerSet) ars := new(v1alpha1.AutoscalingRunnerSet)
Eventually( Eventually(
@ -978,17 +873,10 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
runnerSet := runnerSetList.Items[0] runnerSet := runnerSetList.Items[0]
statusUpdate := runnerSet.DeepCopy() statusUpdate := runnerSet.DeepCopy()
statusUpdate.Status.CurrentReplicas = 6 statusUpdate.Status.Phase = v1alpha1.EphemeralRunnerSetPhaseRunning
statusUpdate.Status.FailedEphemeralRunners = 1
statusUpdate.Status.RunningEphemeralRunners = 2
statusUpdate.Status.PendingEphemeralRunners = 3
desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{ desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{
CurrentRunners: statusUpdate.Status.CurrentReplicas, Phase: v1alpha1.AutoscalingRunnerSetPhaseRunning,
Phase: v1alpha1.AutoscalingRunnerSetPhaseRunning,
PendingEphemeralRunners: statusUpdate.Status.PendingEphemeralRunners,
RunningEphemeralRunners: statusUpdate.Status.RunningEphemeralRunners,
FailedEphemeralRunners: statusUpdate.Status.FailedEphemeralRunners,
} }
err := k8sClient.Status().Patch(ctx, statusUpdate, client.MergeFrom(&runnerSet)) err := k8sClient.Status().Patch(ctx, statusUpdate, client.MergeFrom(&runnerSet))

View File

@ -22,9 +22,12 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "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/controllers/actions.github.com/metrics"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/scaleset" "github.com/actions/scaleset"
"github.com/go-logr/logr" "github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -46,11 +49,19 @@ const (
// EphemeralRunnerReconciler reconciles a EphemeralRunner object // EphemeralRunnerReconciler reconciles a EphemeralRunner object
type EphemeralRunnerReconciler struct { type EphemeralRunnerReconciler struct {
client.Client client.Client
Log logr.Logger Log logr.Logger
Scheme *runtime.Scheme Scheme *runtime.Scheme
PublishMetrics bool
ResourceBuilder ResourceBuilder
} }
var ephemeralRunnerPhaseMetrics = struct {
sync.Mutex
phases map[types.NamespacedName]v1alpha1.EphemeralRunnerPhase
}{
phases: map[types.NamespacedName]v1alpha1.EphemeralRunnerPhase{},
}
// precompute backoff durations for failed ephemeral runners // precompute backoff durations for failed ephemeral runners
// the len(failedRunnerBackoff) must be equal to maxFailures + 1 // the len(failedRunnerBackoff) must be equal to maxFailures + 1
var failedRunnerBackoff = []time.Duration{ var failedRunnerBackoff = []time.Duration{
@ -86,6 +97,8 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
original := ephemeralRunner.DeepCopy() original := ephemeralRunner.DeepCopy()
if !ephemeralRunner.DeletionTimestamp.IsZero() { if !ephemeralRunner.DeletionTimestamp.IsZero() {
r.publishEphemeralRunnerPhaseMetric(&ephemeralRunner, "", log)
if !controllerutil.ContainsFinalizer(&ephemeralRunner, ephemeralRunnerFinalizerName) { if !controllerutil.ContainsFinalizer(&ephemeralRunner, ephemeralRunnerFinalizerName) {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@ -142,6 +155,8 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
r.publishEphemeralRunnerPhaseMetric(&ephemeralRunner, ephemeralRunner.Status.Phase, log)
if ephemeralRunner.IsDone() { if ephemeralRunner.IsDone() {
log.Info("Cleaning up resources after after ephemeral runner termination", "phase", ephemeralRunner.Status.Phase) log.Info("Cleaning up resources after after ephemeral runner termination", "phase", ephemeralRunner.Status.Phase)
err := r.cleanupResources(ctx, &ephemeralRunner, log) err := r.cleanupResources(ctx, &ephemeralRunner, log)
@ -337,6 +352,10 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// 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.
// If side-car container exits with non-zero, it shouldn't affect the runner. Runner exit code // If side-car container exits with non-zero, it shouldn't affect the runner. Runner exit code
// drives the controller's inference of whether the job has succeeded or failed. // drives the controller's inference of whether the job has succeeded or failed.
if err := r.markAsSucceeded(ctx, &ephemeralRunner, pod, log); err != nil {
log.Error(err, "Failed to set ephemeral runner to phase Succeeded")
return ctrl.Result{}, err
}
if err := r.Delete(ctx, &ephemeralRunner); err != nil { if err := r.Delete(ctx, &ephemeralRunner); err != nil {
log.Error(err, "Failed to delete ephemeral runner after successful completion") log.Error(err, "Failed to delete ephemeral runner after successful completion")
return ctrl.Result{}, err return ctrl.Result{}, err
@ -390,6 +409,10 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
default: // succeeded default: // succeeded
log.Info("Ephemeral runner has finished successfully, deleting ephemeral runner", "exitCode", cs.State.Terminated.ExitCode) log.Info("Ephemeral runner has finished successfully, deleting ephemeral runner", "exitCode", cs.State.Terminated.ExitCode)
if err := r.markAsSucceeded(ctx, &ephemeralRunner, pod, log); err != nil {
log.Error(err, "Failed to set ephemeral runner to phase Succeeded")
return ctrl.Result{}, err
}
if err := r.Delete(ctx, &ephemeralRunner); err != nil { if err := r.Delete(ctx, &ephemeralRunner); err != nil {
log.Error(err, "Failed to delete ephemeral runner after successful completion") log.Error(err, "Failed to delete ephemeral runner after successful completion")
return ctrl.Result{}, err return ctrl.Result{}, err
@ -583,6 +606,7 @@ func (r *EphemeralRunnerReconciler) markAsFailed(ctx context.Context, ephemeralR
if err := r.Status().Patch(ctx, ephemeralRunner, client.MergeFrom(original)); err != nil { if err := r.Status().Patch(ctx, ephemeralRunner, client.MergeFrom(original)); err != nil {
return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %w", err) return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %w", err)
} }
r.publishEphemeralRunnerPhaseMetric(ephemeralRunner, ephemeralRunner.Status.Phase, log)
log.Info("Removing the runner from the service") log.Info("Removing the runner from the service")
if err := r.deleteRunnerFromService(ctx, ephemeralRunner, log); err != nil { if err := r.deleteRunnerFromService(ctx, ephemeralRunner, log); err != nil {
@ -604,6 +628,7 @@ func (r *EphemeralRunnerReconciler) markAsOutdated(ctx context.Context, ephemera
if err := r.Status().Patch(ctx, ephemeralRunner, client.MergeFrom(original)); err != nil { if err := r.Status().Patch(ctx, ephemeralRunner, client.MergeFrom(original)); err != nil {
return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %w", err) return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %w", err)
} }
r.publishEphemeralRunnerPhaseMetric(ephemeralRunner, ephemeralRunner.Status.Phase, log)
log.Info("Removing the runner from the service") log.Info("Removing the runner from the service")
if err := r.deleteRunnerFromService(ctx, ephemeralRunner, log); err != nil { if err := r.deleteRunnerFromService(ctx, ephemeralRunner, log); err != nil {
@ -612,6 +637,23 @@ func (r *EphemeralRunnerReconciler) markAsOutdated(ctx context.Context, ephemera
return nil return nil
} }
func (r *EphemeralRunnerReconciler) markAsSucceeded(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, pod *corev1.Pod, log logr.Logger) error {
log.Info("Updating ephemeral runner status to Succeeded")
original := ephemeralRunner.DeepCopy()
ephemeralRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
ephemeralRunner.Status.Ready = false
ephemeralRunner.Status.Reason = pod.Status.Reason
ephemeralRunner.Status.Message = pod.Status.Message
if err := r.Status().Patch(ctx, ephemeralRunner, client.MergeFrom(original)); err != nil {
return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %w", err)
}
r.publishEphemeralRunnerPhaseMetric(ephemeralRunner, ephemeralRunner.Status.Phase, log)
log.Info("EphemeralRunner is marked as Succeeded")
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.
// //
@ -835,11 +877,61 @@ func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context,
if err := r.Status().Patch(ctx, ephemeralRunner, client.MergeFrom(original)); err != nil { if err := r.Status().Patch(ctx, ephemeralRunner, client.MergeFrom(original)); err != nil {
return fmt.Errorf("failed to update runner status for Phase/Reason/Message/Ready: %w", err) return fmt.Errorf("failed to update runner status for Phase/Reason/Message/Ready: %w", err)
} }
r.publishEphemeralRunnerPhaseMetric(ephemeralRunner, ephemeralRunner.Status.Phase, log)
log.Info("Updated ephemeral runner status") log.Info("Updated ephemeral runner status")
return nil return nil
} }
func (r *EphemeralRunnerReconciler) publishEphemeralRunnerPhaseMetric(ephemeralRunner *v1alpha1.EphemeralRunner, phase v1alpha1.EphemeralRunnerPhase, log logr.Logger) {
if !r.PublishMetrics {
return
}
commonLabels, err := ephemeralRunnerMetricLabels(ephemeralRunner)
if err != nil {
log.Error(err, "Failed to build ephemeral runner metric labels")
return
}
key := types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}
ephemeralRunnerPhaseMetrics.Lock()
defer ephemeralRunnerPhaseMetrics.Unlock()
previousPhase, ok := ephemeralRunnerPhaseMetrics.phases[key]
if ok && previousPhase == phase {
return
}
if ok {
metrics.SubEphemeralRunner(commonLabels, previousPhase)
}
if phase == "" {
delete(ephemeralRunnerPhaseMetrics.phases, key)
return
}
metrics.AddEphemeralRunner(commonLabels, phase)
ephemeralRunnerPhaseMetrics.phases[key] = phase
}
func ephemeralRunnerMetricLabels(ephemeralRunner *v1alpha1.EphemeralRunner) (metrics.CommonLabels, error) {
parsedURL, err := actions.ParseGitHubConfigFromURL(ephemeralRunner.Spec.GitHubConfigURL)
if err != nil {
return metrics.CommonLabels{}, fmt.Errorf("github config URL is invalid: %w", err)
}
return metrics.CommonLabels{
Name: ephemeralRunner.Labels[LabelKeyGitHubScaleSetName],
Namespace: ephemeralRunner.Labels[LabelKeyGitHubScaleSetNamespace],
Repository: parsedURL.Repository,
Organization: parsedURL.Organization,
Enterprise: parsedURL.Enterprise,
}, nil
}
func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error { func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error {
client, err := r.GetActionsService(ctx, ephemeralRunner) client, err := r.GetActionsService(ctx, ephemeralRunner)
if err != nil { if err != nil {

View File

@ -27,9 +27,7 @@ import (
"time" "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/controllers/actions.github.com/metrics"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient" "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/scaleset" "github.com/actions/scaleset"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"go.uber.org/multierr" "go.uber.org/multierr"
@ -51,9 +49,8 @@ const (
// EphemeralRunnerSetReconciler reconciles a EphemeralRunnerSet object // EphemeralRunnerSetReconciler reconciles a EphemeralRunnerSet object
type EphemeralRunnerSetReconciler struct { type EphemeralRunnerSetReconciler struct {
client.Client client.Client
Log logr.Logger Log logr.Logger
Scheme *runtime.Scheme Scheme *runtime.Scheme
PublishMetrics bool
ResourceBuilder ResourceBuilder
} }
@ -205,29 +202,6 @@ func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.R
"deleting", len(ephemeralRunnersByState.deleting), "deleting", len(ephemeralRunnersByState.deleting),
) )
if r.PublishMetrics {
githubConfigURL := ephemeralRunnerSet.Spec.EphemeralRunnerSpec.GitHubConfigURL
parsedURL, err := actions.ParseGitHubConfigFromURL(githubConfigURL)
if err != nil {
log.Error(err, "Github Config URL is invalid", "URL", githubConfigURL)
// stop reconciling on this object
return ctrl.Result{}, nil
}
metrics.SetEphemeralRunnerCountsByStatus(
metrics.CommonLabels{
Name: ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetName],
Namespace: ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetNamespace],
Repository: parsedURL.Repository,
Organization: parsedURL.Organization,
Enterprise: parsedURL.Enterprise,
},
len(ephemeralRunnersByState.pending),
len(ephemeralRunnersByState.running),
len(ephemeralRunnersByState.failed),
)
}
total := ephemeralRunnersByState.scaleTotal() total := ephemeralRunnersByState.scaleTotal()
if ephemeralRunnerSet.Spec.PatchID == 0 || ephemeralRunnerSet.Spec.PatchID != ephemeralRunnersByState.latestPatchID { if ephemeralRunnerSet.Spec.PatchID == 0 || ephemeralRunnerSet.Spec.PatchID != ephemeralRunnersByState.latestPatchID {
defer func() { defer func() {
@ -272,7 +246,6 @@ func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.R
func (r *EphemeralRunnerSetReconciler) updateStatus(ctx context.Context, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, state *ephemeralRunnersByState, log logr.Logger) error { func (r *EphemeralRunnerSetReconciler) updateStatus(ctx context.Context, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, state *ephemeralRunnersByState, log logr.Logger) error {
original := ephemeralRunnerSet.DeepCopy() original := ephemeralRunnerSet.DeepCopy()
total := state.scaleTotal()
var phase v1alpha1.EphemeralRunnerSetPhase var phase v1alpha1.EphemeralRunnerSetPhase
switch { switch {
case len(state.outdated) > 0: case len(state.outdated) > 0:
@ -283,11 +256,7 @@ func (r *EphemeralRunnerSetReconciler) updateStatus(ctx context.Context, ephemer
phase = ephemeralRunnerSet.Status.Phase phase = ephemeralRunnerSet.Status.Phase
} }
desiredStatus := v1alpha1.EphemeralRunnerSetStatus{ desiredStatus := v1alpha1.EphemeralRunnerSetStatus{
CurrentReplicas: total, Phase: phase,
Phase: phase,
PendingEphemeralRunners: len(state.pending),
RunningEphemeralRunners: len(state.running),
FailedEphemeralRunners: len(state.failed),
} }
// Update the status if needed. // Update the status if needed.

View File

@ -9,14 +9,21 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/metrics"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/scaleset"
prometheusdto "github.com/prometheus/client_model/go"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors" kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
controllerMetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
@ -34,10 +41,92 @@ const (
ephemeralRunnerSetTestInterval = time.Millisecond * 250 ephemeralRunnerSetTestInterval = time.Millisecond * 250
) )
var registerActionsGithubMetricsForTest sync.Once
func TestPrecomputedConstants(t *testing.T) { func TestPrecomputedConstants(t *testing.T) {
require.Equal(t, len(failedRunnerBackoff), maxFailures+1) require.Equal(t, len(failedRunnerBackoff), maxFailures+1)
} }
func expectEphemeralRunnerPhase(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, phase v1alpha1.EphemeralRunnerPhase) {
updated := new(v1alpha1.EphemeralRunner)
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
Expect(err).NotTo(HaveOccurred(), "failed to get ephemeral runner")
Expect(updated.Status.Phase).To(Equal(phase))
}
func expectEphemeralRunnerPhaseMetric(ephemeralRunner *v1alpha1.EphemeralRunner, phase v1alpha1.EphemeralRunnerPhase, expected float64) {
metricName := ephemeralRunnerPhaseMetricName(phase)
Expect(metricName).NotTo(BeEmpty(), "unexpected ephemeral runner phase")
parsedURL, err := actions.ParseGitHubConfigFromURL(ephemeralRunner.Spec.GitHubConfigURL)
Expect(err).NotTo(HaveOccurred(), "failed to parse GitHub config URL")
value, err := ephemeralRunnerPhaseMetricValue(metricName, map[string]string{
"name": ephemeralRunner.Labels[LabelKeyGitHubScaleSetName],
"namespace": ephemeralRunner.Labels[LabelKeyGitHubScaleSetNamespace],
"repository": parsedURL.Repository,
"organization": parsedURL.Organization,
"enterprise": parsedURL.Enterprise,
})
Expect(err).NotTo(HaveOccurred(), "failed to gather ephemeral runner phase metrics")
Expect(value).To(Equal(expected))
}
func ephemeralRunnerPhaseMetricName(phase v1alpha1.EphemeralRunnerPhase) string {
switch phase {
case v1alpha1.EphemeralRunnerPhasePending:
return "gha_controller_pending_ephemeral_runners"
case v1alpha1.EphemeralRunnerPhaseRunning:
return "gha_controller_running_ephemeral_runners"
case v1alpha1.EphemeralRunnerPhaseSucceeded:
return "gha_controller_succeeded_ephemeral_runners"
case v1alpha1.EphemeralRunnerPhaseFailed:
return "gha_controller_failed_ephemeral_runners"
case v1alpha1.EphemeralRunnerPhaseOutdated:
return "gha_controller_outdated_ephemeral_runners"
default:
return ""
}
}
func ephemeralRunnerPhaseMetricValue(metricName string, labels map[string]string) (float64, error) {
metricFamilies, err := controllerMetrics.Registry.Gather()
if err != nil {
return 0, err
}
for _, metricFamily := range metricFamilies {
if metricFamily.GetName() != metricName {
continue
}
for _, metric := range metricFamily.GetMetric() {
if metricHasLabels(metric, labels) {
if metric.GetGauge() == nil {
return 0, nil
}
return metric.GetGauge().GetValue(), nil
}
}
}
return 0, nil
}
func metricHasLabels(metric *prometheusdto.Metric, labels map[string]string) bool {
metricLabels := map[string]string{}
for _, label := range metric.GetLabel() {
metricLabels[label.GetName()] = label.GetValue()
}
for name, value := range labels {
if metricLabels[name] != value {
return false
}
}
return true
}
var _ = Describe("Test EphemeralRunnerSet controller", func() { var _ = Describe("Test EphemeralRunnerSet controller", func() {
var ctx context.Context var ctx context.Context
var mgr ctrl.Manager var mgr ctrl.Manager
@ -133,20 +222,20 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
ephemeralRunnerSetTestInterval, ephemeralRunnerSetTestInterval,
).Should(BeEquivalentTo(0), "No EphemeralRunner should be created") ).Should(BeEquivalentTo(0), "No EphemeralRunner should be created")
// Check if the status stay 0 // Check if the status is initialized
Consistently( Consistently(
func() (int, error) { func() (v1alpha1.EphemeralRunnerSetPhase, error) {
runnerSet := new(v1alpha1.EphemeralRunnerSet) runnerSet := new(v1alpha1.EphemeralRunnerSet)
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, runnerSet) err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, runnerSet)
if err != nil { if err != nil {
return -1, err return "", err
} }
return int(runnerSet.Status.CurrentReplicas), nil return runnerSet.Status.Phase, nil
}, },
ephemeralRunnerSetTestTimeout, ephemeralRunnerSetTestTimeout,
ephemeralRunnerSetTestInterval, ephemeralRunnerSetTestInterval,
).Should(BeEquivalentTo(0), "EphemeralRunnerSet status should be 0") ).Should(BeEquivalentTo(v1alpha1.EphemeralRunnerSetPhaseRunning), "EphemeralRunnerSet status should be running")
// Scaling up the EphemeralRunnerSet // Scaling up the EphemeralRunnerSet
updated := created.DeepCopy() updated := created.DeepCopy()
@ -187,20 +276,20 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
ephemeralRunnerSetTestInterval, ephemeralRunnerSetTestInterval,
).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created") ).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created")
// Check if the status is updated // Check if the status stays running
Eventually( Eventually(
func() (int, error) { func() (v1alpha1.EphemeralRunnerSetPhase, error) {
runnerSet := new(v1alpha1.EphemeralRunnerSet) runnerSet := new(v1alpha1.EphemeralRunnerSet)
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, runnerSet) err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, runnerSet)
if err != nil { if err != nil {
return -1, err return "", err
} }
return int(runnerSet.Status.CurrentReplicas), nil return runnerSet.Status.Phase, nil
}, },
ephemeralRunnerSetTestTimeout, ephemeralRunnerSetTestTimeout,
ephemeralRunnerSetTestInterval, ephemeralRunnerSetTestInterval,
).Should(BeEquivalentTo(5), "EphemeralRunnerSet status should be 5") ).Should(BeEquivalentTo(v1alpha1.EphemeralRunnerSetPhaseRunning), "EphemeralRunnerSet status should be running")
}) })
}) })
@ -1184,11 +1273,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, Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
CurrentReplicas: 3,
PendingEphemeralRunners: 1,
RunningEphemeralRunners: 1,
FailedEphemeralRunners: 1,
} }
Eventually( Eventually(
func() (v1alpha1.EphemeralRunnerSetStatus, error) { func() (v1alpha1.EphemeralRunnerSetStatus, error) {
@ -1227,11 +1312,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
).Should(BeEquivalentTo(1), "Failed to eventually scale down") ).Should(BeEquivalentTo(1), "Failed to eventually scale down")
desiredStatus = v1alpha1.EphemeralRunnerSetStatus{ desiredStatus = v1alpha1.EphemeralRunnerSetStatus{
CurrentReplicas: 1, Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
PendingEphemeralRunners: 0,
RunningEphemeralRunners: 0,
FailedEphemeralRunners: 1,
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
} }
Eventually( Eventually(
@ -1251,11 +1332,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
Expect(err).To(BeNil(), "Failed to delete failed ephemeral runner") Expect(err).To(BeNil(), "Failed to delete failed ephemeral runner")
desiredStatus = v1alpha1.EphemeralRunnerSetStatus{ desiredStatus = v1alpha1.EphemeralRunnerSetStatus{
CurrentReplicas: 0, Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
PendingEphemeralRunners: 0,
RunningEphemeralRunners: 0,
FailedEphemeralRunners: 0,
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
} }
Eventually( Eventually(
func() (v1alpha1.EphemeralRunnerSetStatus, error) { func() (v1alpha1.EphemeralRunnerSetStatus, error) {
@ -1273,6 +1350,148 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
}) })
}) })
var _ = Describe("EphemeralRunner phase metrics", func() {
var ctx context.Context
var autoscalingNS *corev1.Namespace
var mgr ctrl.Manager
var configSecret *corev1.Secret
var controller *EphemeralRunnerReconciler
var ephemeralRunner *v1alpha1.EphemeralRunner
var request ctrl.Request
BeforeEach(func() {
registerActionsGithubMetricsForTest.Do(func() {
metrics.RegisterMetrics()
})
ephemeralRunnerPhaseMetrics.Lock()
ephemeralRunnerPhaseMetrics.phases = map[types.NamespacedName]v1alpha1.EphemeralRunnerPhase{}
ephemeralRunnerPhaseMetrics.Unlock()
ctx = context.Background()
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
controller = &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: mgr.GetScheme(),
Log: logf.Log,
PublishMetrics: true,
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, fake.NewMultiClient(
fake.WithClient(
fake.NewClient(
fake.WithGenerateJitRunnerConfig(
&scaleset.RunnerScaleSetJitRunnerConfig{
Runner: &scaleset.RunnerReference{ID: 100, Name: "test-runner"},
EncodedJITConfig: "fake-jit-config",
},
nil,
),
),
),
)),
},
}
ephemeralRunner = &v1alpha1.EphemeralRunner{
ObjectMeta: metav1.ObjectMeta{
Name: "test-runner",
Namespace: autoscalingNS.Name,
Labels: map[string]string{
LabelKeyGitHubScaleSetName: "test-scale-set",
LabelKeyGitHubScaleSetNamespace: autoscalingNS.Name,
},
},
Spec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigURL: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name,
RunnerScaleSetID: 100,
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: v1alpha1.EphemeralRunnerContainerName,
Image: "ghcr.io/actions/runner",
},
},
},
},
},
}
err := k8sClient.Create(ctx, ephemeralRunner)
Expect(err).NotTo(HaveOccurred(), "failed to create ephemeral runner")
request = ctrl.Request{NamespacedName: types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}}
})
It("publishes pending, running, and succeeded phase transitions", func() {
_, err := controller.Reconcile(ctx, request)
Expect(err).NotTo(HaveOccurred(), "failed to reconcile ephemeral runner")
pod := new(corev1.Pod)
Eventually(func() error {
return k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
}, ephemeralRunnerSetTestTimeout, ephemeralRunnerSetTestInterval).Should(Succeed(), "expected ephemeral runner pod to be created")
podPending := pod.DeepCopy()
podPending.Status.Phase = corev1.PodPending
podPending.Status.ContainerStatuses = []corev1.ContainerStatus{
{
Name: v1alpha1.EphemeralRunnerContainerName,
State: corev1.ContainerState{},
},
}
err = k8sClient.Status().Patch(ctx, podPending, client.MergeFrom(pod))
Expect(err).NotTo(HaveOccurred(), "failed to patch pod to pending")
_, err = controller.Reconcile(ctx, request)
Expect(err).NotTo(HaveOccurred(), "failed to reconcile pending pod")
expectEphemeralRunnerPhase(ctx, ephemeralRunner, v1alpha1.EphemeralRunnerPhasePending)
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhasePending, 1)
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhaseRunning, 0)
podRunning := podPending.DeepCopy()
podRunning.Status.Phase = corev1.PodRunning
podRunning.Status.ContainerStatuses = []corev1.ContainerStatus{
{
Name: v1alpha1.EphemeralRunnerContainerName,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{StartedAt: metav1.Now()},
},
},
}
err = k8sClient.Status().Patch(ctx, podRunning, client.MergeFrom(podPending))
Expect(err).NotTo(HaveOccurred(), "failed to patch pod to running")
_, err = controller.Reconcile(ctx, request)
Expect(err).NotTo(HaveOccurred(), "failed to reconcile running pod")
expectEphemeralRunnerPhase(ctx, ephemeralRunner, v1alpha1.EphemeralRunnerPhaseRunning)
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhasePending, 0)
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhaseRunning, 1)
podSucceeded := podRunning.DeepCopy()
podSucceeded.Status.Phase = corev1.PodSucceeded
podSucceeded.Status.ContainerStatuses = []corev1.ContainerStatus{
{
Name: v1alpha1.EphemeralRunnerContainerName,
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{ExitCode: 0},
},
},
}
err = k8sClient.Status().Patch(ctx, podSucceeded, client.MergeFrom(podRunning))
Expect(err).NotTo(HaveOccurred(), "failed to patch pod to succeeded")
_, err = controller.Reconcile(ctx, request)
Expect(err).NotTo(HaveOccurred(), "failed to reconcile succeeded pod")
expectEphemeralRunnerPhase(ctx, ephemeralRunner, v1alpha1.EphemeralRunnerPhaseSucceeded)
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhaseRunning, 0)
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhaseSucceeded, 1)
})
})
var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func() { var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func() {
var ctx context.Context var ctx context.Context
var mgr ctrl.Manager var mgr ctrl.Manager

View File

@ -1,6 +1,7 @@
package metrics package metrics
import ( import (
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/metrics" "sigs.k8s.io/controller-runtime/pkg/metrics"
) )
@ -50,6 +51,14 @@ var (
}, },
labels, labels,
) )
succeededEphemeralRunners = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Subsystem: githubScaleSetControllerSubsystem,
Name: "succeeded_ephemeral_runners",
Help: "Number of ephemeral runners in a succeeded state.",
},
labels,
)
failedEphemeralRunners = prometheus.NewGaugeVec( failedEphemeralRunners = prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Subsystem: githubScaleSetControllerSubsystem, Subsystem: githubScaleSetControllerSubsystem,
@ -58,6 +67,14 @@ var (
}, },
labels, labels,
) )
outdatedEphemeralRunners = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Subsystem: githubScaleSetControllerSubsystem,
Name: "outdated_ephemeral_runners",
Help: "Number of ephemeral runners in an outdated state.",
},
labels,
)
runningListeners = prometheus.NewGaugeVec( runningListeners = prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Subsystem: githubScaleSetControllerSubsystem, Subsystem: githubScaleSetControllerSubsystem,
@ -72,15 +89,34 @@ func RegisterMetrics() {
metrics.Registry.MustRegister( metrics.Registry.MustRegister(
pendingEphemeralRunners, pendingEphemeralRunners,
runningEphemeralRunners, runningEphemeralRunners,
succeededEphemeralRunners,
failedEphemeralRunners, failedEphemeralRunners,
outdatedEphemeralRunners,
runningListeners, runningListeners,
) )
} }
func SetEphemeralRunnerCountsByStatus(commonLabels CommonLabels, pending, running, failed int) { func AddEphemeralRunner(commonLabels CommonLabels, phase v1alpha1.EphemeralRunnerPhase) {
pendingEphemeralRunners.With(commonLabels.labels()).Set(float64(pending)) updateEphemeralRunner(commonLabels, phase, 1)
runningEphemeralRunners.With(commonLabels.labels()).Set(float64(running)) }
failedEphemeralRunners.With(commonLabels.labels()).Set(float64(failed))
func SubEphemeralRunner(commonLabels CommonLabels, phase v1alpha1.EphemeralRunnerPhase) {
updateEphemeralRunner(commonLabels, phase, -1)
}
func updateEphemeralRunner(commonLabels CommonLabels, phase v1alpha1.EphemeralRunnerPhase, delta float64) {
switch phase {
case v1alpha1.EphemeralRunnerPhasePending:
pendingEphemeralRunners.With(commonLabels.labels()).Add(delta)
case v1alpha1.EphemeralRunnerPhaseRunning:
runningEphemeralRunners.With(commonLabels.labels()).Add(delta)
case v1alpha1.EphemeralRunnerPhaseSucceeded:
succeededEphemeralRunners.With(commonLabels.labels()).Add(delta)
case v1alpha1.EphemeralRunnerPhaseFailed:
failedEphemeralRunners.With(commonLabels.labels()).Add(delta)
case v1alpha1.EphemeralRunnerPhaseOutdated:
outdatedEphemeralRunners.With(commonLabels.labels()).Add(delta)
}
} }
func AddRunningListener(commonLabels CommonLabels) { func AddRunningListener(commonLabels CommonLabels) {

View File

@ -1,19 +1,19 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIDKjCCAhKgAwIBAgIUQr7R8yN5+2and6ucUOPF6oIbD44wDQYJKoZIhvcNAQEL MIIDKjCCAhKgAwIBAgIUVy/UETGamzkzSYS3TortnzNWCekwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI1MDIyODEyMDEzMFoXDTI3 BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI2MDcxMzEzMjQwN1oXDTI5
MTEyNTEyMDEzMFowHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwggEi MDQwODEzMjQwN1owHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOGvN95wCkYO35qyJnf/RwTiDb MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDzbccB7wpLODrRW3IePtAmKk63
oEVaefKnZZny1JrO34MFjlAz8C/P5WwxNUzzbQLTPh5iTqFRU+vis6HPvV0HJEoI 8pVYb9yZZL5KjLjli+xpWURvS1N8X44NyU67nkArsO3UG3kq8KGYU03Dy5iCQmlT
wTfgBCZxcdY8fEIY96FGHLju3PzfxeJaVHyi+2cTtzU+oNp4OFF8huApjYXjaV4y hXddmpNIAc56CF/vYBh7hzklrF1qFMgwHl48LsemiYfn+vizonzd+UmrKqhghDRy
pAirPbiiP/cgtcT4L5WErQi0aGZkq+1YqY2duNFNIGPTEcXV4iN4IhuD9dpqdKFg SwNLnzSCWBgWwaJPyKlp9pUXpxmSjU+1ByRaLl5E2tmCA3ghdJPXayQeZz+nWcxv
H0wmZDgH+VE/5ACXovU8j5cxCKOJGxTVMKVZlvxPH3w69Z85x3o5AAnyxwo8E2zo PWzo5EyD5SVm45WYxPKuFalAmmwMnSZxcTBvl/SlHhSR5o40bQ/KOqkBHq/rpEAK
TC1FJ1eFLsmYLZki6cGBzSkIl5QlLGHakWYh+JLu/pkfTL8t+AkY3hZJM96ZAgMB ps7W9pnn2xgq3KOTf554ymIqTyfdInlOkh+2b2joVIS2/fVVDuTpeGn4NE7DAgMB
AAGjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1Ud AAGjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1Ud
DgQWBBSmAyntm95+KoyL3ffLBXZKSpk1VTAfBgNVHSMEGDAWgBR93+rQFjh+RUFX DgQWBBTjYR9enoN6IY3lzzq7hF4R3Pxr7jAfBgNVHSMEGDAWgBS92jtC5lTGLF1Y
df4dbNcfS2hbTzANBgkqhkiG9w0BAQsFAAOCAQEABopVFLGQf/LFH+OKVCOT8FCC ZRVMVU7lo4qtbDANBgkqhkiG9w0BAQsFAAOCAQEAlFifQlOXNpUQxbSXmjZj40dW
y/+o1B/U5jXVvbfwlSGScaiJGQ94FsuH59XJCGySQj77ZVTeElBtntoLXmOCFjyF kXhm3s7rBfv6jLm7ebGkbKQ9SPqM94hi9nFX2BWD9DKltHJgmv6mvwWYj0/YO5ij
jKHCDfUpB4nzeqNMvTDzuoYyPS8DhoGfEnaCgJyKf6GU4p41502gH8mQRB7azzL7 /A7ckNy3Xf7vu1L3h9CUViZM8d9HMCAnneQihF8X0W+yUwHVhZTFDedl4JMWcttL
5jW0aFatCA6G6T1oogHZpHf0ice80C2JkFbWHSE9JxqARbTc06wCDBiSBFTGZQDO zHMtwlhbaFZhPolkvkNksZRvAOak2O6LUOURvw/alt3tVI/zOyomH5DYi7uxMXXj
JaBIbn6FL3zSkKcpwgJEqDRavVuoDUlJPDqtTzjf/fMQGGR2LUFkceJpsQqf1jrF H7/61rLDdE5cLWIgpcCDDKQi1ORP6w5/HaQ5+1G/IA2PMN0jS903hvxJqTki3XLi
1yTtEZ8gjR2g2Vj6IszUAgbc87xR0AgyGDVckiUdhlX2Y6KCqo2cl9LfSVpqtw== uygYXVa4HETKowgXWXLsNHnMHExpE6kPPtU9v/1t3Lah5H/D3qgicZ1DvMNMaA==
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -1,20 +1,20 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIDQjCCAiqgAwIBAgIUHT3JtqsYKs7NHv1LNyS9RYC7vsAwDQYJKoZIhvcNAQEL MIIDQjCCAiqgAwIBAgIUbiA5aoS2wqoRlN7qlS1CUrQRrZIwDQYJKoZIhvcNAQEL
BQAwHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMjUwMjI4MTIw BQAwHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMjYwNzEzMTMy
MTMwWhcNMjYwNzEzMTIwMTMwWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0G NDA3WhcNMjcxMTI1MTMyNDA3WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHwI/xSLgCuQrx+WsvupA8w4eMdSef CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDf2xz5c/Kts7iYFbznIBqnos0OZw1m
WGw523OJYPJkDYJGgSGsjVb9htba6vXYbGNohuluEAZIyT7GvmPezTokeVMkuSYT XqQRfd8Ub7mcRcNu5LAS+QXaMCQrtSS8HaiOWrnmXVnY0Dtm9BYYgxCLbffBjPgv
lSV8xplFEtDlQhTzaI/cofbi7qtT91/5zS/w0JSaNosThGtZg/M4ZOiMj04m0NGK CjMURftuI0fvFfFHhNOeuO1Y6N3z4YFY9UHldiqBIKmzrEKl6ZzibbHhIWlOFdMC
Zz56l9Lpe/yM7fPda++D9xYEGSSdwK9CqqwF+cXN09d6IK1VINIIjT3Sdb9Sssok IHosnO+9LgXgEcr242yrBczl2SWycXG4OGag4yp2FrsvrKtdqyErJBuyIQHv51k6
GWmD7UUPLvwZ5379+HRs1K8AFXqvbkeWVYtrJwJMxJGVnNSeiqKGSmMEpP7tVNHl Eo5vz42UxZCcjBciSe/X2Tf+KDLzslZDfnY+8PWU2Dx5DaMZAxNVa78jRDhKact9
s4V7oyQXd8KX+HpziiayjGy9giVteJJi/bAmUp+0+hTHBes5fOWI5JyDAgMBAAGj jOunKd1JRj/GZkV5EKL/lIJJQPgcNCl0s8swKlicEYpqEdS4Ayj0fF7DAgMBAAGj
gYAwfjAfBgNVHSMEGDAWgBSmAyntm95+KoyL3ffLBXZKSpk1VTAJBgNVHRMEAjAA gYAwfjAfBgNVHSMEGDAWgBTjYR9enoN6IY3lzzq7hF4R3Pxr7jAJBgNVHRMEAjAA
MAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/ MAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/
AAABMB0GA1UdDgQWBBTVdJE2lkGsNIU3LlEf3rN5fyaRkjANBgkqhkiG9w0BAQsF AAABMB0GA1UdDgQWBBQtvGpvrghOSrZI4B1ieqKJc69RVDANBgkqhkiG9w0BAQsF
AAOCAQEAo1klH9WMsPWTN9qN3tdud07eatulEKo/0okaph6MJ59ozseOzxrfpwL0 AAOCAQEAm33hxRyoocCNH2OUhzogkSbi59+an4bLV07uTm3thrdhNKefsPlt1D6Q
67Nr8yl+VwZqrRTBurp0n6G+n0j8UHfWjSrAqN4yUHl+heT0HpnLR2FE9YgZEmxR 11QTafhH8RAjlnn/PavLgk3WpTvuC4imlPfhxjf1gRg0zJXW3y0GC2wizSm5cXdK
bPfVbPBef/eJeE7/U6imfBYzzMajua+hg05sVHUNNdPaFOP+Xj47x8uQmf9w5/kf L1ikKpLd17Gys4kGwMLqxCZunpGQnoJpEWBrg3kc7RPp6rgLRMAuj/ZwUzV9nNiU
MrylRUSgH5RRge4+2T5hmNM9tHfF6OfDHitrXnl+X6h/x/tkBvDcUXtKa5xuEcSg YUA64FYY4ISIZigYGCkJUpqgyUQYsD+lYWZl2QlVje1riXIPReIBxd4kcMLqng0l
WpmJKl3pKfXvdmCIrj9Vca+UD2Bntkk2jgDTLEPJAxMgrsQRhnUJclaunnd1NQbc IysTAnZ+vNMWVLjhthGlOfs0yDaIoDa2HHskdaGtlz97SPfIUlDJ/20QLDMZEv0M
FmjFW7iaNvDVKt+vYqH8ff8U9iCB2g== uSjtuu8PGy+iMI5+SfBE9D4cF1tn0w==
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -1,28 +1,28 @@
-----BEGIN PRIVATE KEY----- -----BEGIN PRIVATE KEY-----
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
LG1OvFTywkxBuOUKsNsErQ== PEj26Z+E4YTsviOuWu7EkYQ=
-----END PRIVATE KEY----- -----END PRIVATE KEY-----

View File

@ -1,19 +1,19 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -1,20 +1,20 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
e7KoqGzWXvWLqJ13a9U= IzEG5rEh7Ypi6s+ic8s=
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -1,28 +1,28 @@
-----BEGIN PRIVATE KEY----- -----BEGIN PRIVATE KEY-----
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
xfbmGepRVWXw2dLmxhA9/zM= 8aUFUg2z34K+VpDf4D9qWd4=
-----END PRIVATE KEY----- -----END PRIVATE KEY-----

View File

@ -340,6 +340,7 @@ func main() {
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: log.WithName("EphemeralRunner").WithValues("version", build.Version), Log: log.WithName("EphemeralRunner").WithValues("version", build.Version),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
PublishMetrics: metricsAddr != "0",
ResourceBuilder: rb, ResourceBuilder: rb,
}).SetupWithManager(mgr, runnerOpts...); err != nil { }).SetupWithManager(mgr, runnerOpts...); err != nil {
log.Error(err, "unable to create controller", "controller", "EphemeralRunner") log.Error(err, "unable to create controller", "controller", "EphemeralRunner")
@ -350,7 +351,6 @@ func main() {
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: log.WithName("EphemeralRunnerSet").WithValues("version", build.Version), Log: log.WithName("EphemeralRunnerSet").WithValues("version", build.Version),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
PublishMetrics: metricsAddr != "0",
ResourceBuilder: rb, ResourceBuilder: rb,
}).SetupWithManager(mgr, controllerOpts...); err != nil { }).SetupWithManager(mgr, controllerOpts...); err != nil {
log.Error(err, "unable to create controller", "controller", "EphemeralRunnerSet") log.Error(err, "unable to create controller", "controller", "EphemeralRunnerSet")