Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
Expand Down Expand Up @@ -873,8 +874,8 @@ func (r *AutoscalingListenerReconciler) SetupWithManager(mgr ctrl.Manager, opts
return builderWithOptions(
ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.AutoscalingListener{}).
Owns(&corev1.Pod{}).
Owns(&corev1.ServiceAccount{}).
Owns(&corev1.Pod{}, builder.WithPredicates(autoscalingListenerOwnedPodPredicate())).
Owns(&corev1.ServiceAccount{}, builder.WithPredicates(autoscalingListenerOwnedServiceAccountPredicate())).
Watches(&rbacv1.Role{}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
Watches(&rbacv1.RoleBinding{}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
Expand Down Expand Up @@ -888,7 +889,7 @@ func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager, opts
return builderWithOptions(
ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.AutoscalingRunnerSet{}).
Owns(&v1alpha1.EphemeralRunnerSet{}).
Owns(&v1alpha1.EphemeralRunnerSet{}, builder.WithPredicates(autoscalingRunnerSetOwnedEphemeralRunnerSetPredicate())).
Watches(&v1alpha1.AutoscalingListener{}, handler.EnqueueRequestsFromMapFunc(
func(_ context.Context, o client.Object) []reconcile.Request {
autoscalingListener := o.(*v1alpha1.AutoscalingListener)
Expand Down
12 changes: 3 additions & 9 deletions controllers/actions.github.com/ephemeralrunner_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/predicate"
Expand Down Expand Up @@ -843,14 +844,7 @@ func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context,
return nil
}

var ready bool
var lastTransitionTime time.Time
for _, condition := range pod.Status.Conditions {
if condition.Type == corev1.PodReady && condition.LastTransitionTime.After(lastTransitionTime) {
ready = condition.Status == corev1.ConditionTrue
lastTransitionTime = condition.LastTransitionTime.Time
}
}
ready := podReady(pod)

// Publish Pending as soon as the runner is observed non-terminal, regardless of
// the pod phase. The controller only reaches this point once the runner
Expand Down Expand Up @@ -971,7 +965,7 @@ func (r *EphemeralRunnerReconciler) SetupWithManager(mgr ctrl.Manager, opts ...O
return builderWithOptions(
ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.EphemeralRunner{}).
Owns(&corev1.Pod{}).
Owns(&corev1.Pod{}, builder.WithPredicates(ephemeralRunnerOwnedPodPredicate())).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}),
opts,
).Complete(r)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/predicate"
Expand Down Expand Up @@ -981,7 +982,7 @@ func (r *EphemeralRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager, opts .
return builderWithOptions(
ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.EphemeralRunnerSet{}).
Owns(&v1alpha1.EphemeralRunner{}).
Owns(&v1alpha1.EphemeralRunner{}, builder.WithPredicates(ephemeralRunnerSetOwnedEphemeralRunnerPredicate())).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}),
opts,
).Complete(r)
Expand Down
222 changes: 222 additions & 0 deletions controllers/actions.github.com/predicates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package actionsgithubcom

import (
"slices"
"time"

"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// The predicates in this file exist purely to keep work out of the workqueue.
// They must never change what a reconciler does, so each of them is written as
// the projection of the fields its reconciler actually reads: an update is
// dropped only when every one of those fields is unchanged. Whenever a
// reconciler starts reading a new field, the matching projection below has to
Comment thread
nikola-jokic marked this conversation as resolved.
// grow with it.
//
// Create, delete and generic events are always delivered. Only updates are
// considered here.

// autoscalingRunnerSetOwnedEphemeralRunnerSetPredicate filters updates of the
// EphemeralRunnerSets owned by an AutoscalingRunnerSet.
//
// The AutoscalingRunnerSet reconciler reads the runner set's object metadata
// (labels, annotations, finalizers and deletion timestamp), its whole spec, and,
// through ephemeralRunnerSetOutdatedForAppliedRevision, Status.Phase together
// with Status.AppliedActionableRevision. It never reads
// Status.FinishedRunnerCleanupPatchID, which the EphemeralRunnerSet rewrites for
// every listener patch id and which is the noisiest part of the object.
func autoscalingRunnerSetOwnedEphemeralRunnerSetPredicate() predicate.Predicate {
return predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oldRunnerSet, oldOk := e.ObjectOld.(*v1alpha1.EphemeralRunnerSet)
newRunnerSet, newOk := e.ObjectNew.(*v1alpha1.EphemeralRunnerSet)
if !oldOk || !newOk {
// Not the type we reasoned about, so we cannot claim the event
// is irrelevant. Let it through.
return true
}

if !equalReconciledObjectMeta(&oldRunnerSet.ObjectMeta, &newRunnerSet.ObjectMeta) ||
!equality.Semantic.DeepEqual(&oldRunnerSet.Spec, &newRunnerSet.Spec) {
Comment on lines +45 to +46

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fine, this should not influence the controller in any meaningful way

return true
}

return oldRunnerSet.Status.Phase != newRunnerSet.Status.Phase ||
oldRunnerSet.Status.AppliedActionableRevision != newRunnerSet.Status.AppliedActionableRevision
},
}
}

// ephemeralRunnerSetOwnedEphemeralRunnerPredicate filters updates of the
// EphemeralRunners owned by an EphemeralRunnerSet.
//
// Besides object metadata and spec, the EphemeralRunnerSet reconciler reads
// Status.Phase, to group runners by state, Status.RunnerID, to decide whether a
// runner still has to be removed from the service, and Status.JobID, through
// HasJob, to skip runners that are busy serving a job. The rest of the runner
// status (readiness, failure bookkeeping, reason, message and the remaining job
// details written by the listener) is never read, and it is by far the noisiest
// part of the object.
func ephemeralRunnerSetOwnedEphemeralRunnerPredicate() predicate.Predicate {
return predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oldRunner, oldOk := e.ObjectOld.(*v1alpha1.EphemeralRunner)
newRunner, newOk := e.ObjectNew.(*v1alpha1.EphemeralRunner)
if !oldOk || !newOk {
return true
}

if !equalReconciledObjectMeta(&oldRunner.ObjectMeta, &newRunner.ObjectMeta) ||
!equality.Semantic.DeepEqual(&oldRunner.Spec, &newRunner.Spec) {
return true
}

return oldRunner.Status.Phase != newRunner.Status.Phase ||
oldRunner.Status.RunnerID != newRunner.Status.RunnerID ||
oldRunner.Status.JobID != newRunner.Status.JobID
},
}
}

// ephemeralRunnerOwnedPodPredicate filters updates of the pod owned by an
// EphemeralRunner.
//
// The EphemeralRunner reconciler reads the pod UID, its deletion timestamp, the
// pod phase, reason and message, the container and init container statuses, and
// the Ready condition. It never reads the pod spec or the remaining status
// fields, which is where most pod updates land: assigned IPs, the node the pod
// was scheduled on, start time and the conditions other than Ready.
func ephemeralRunnerOwnedPodPredicate() predicate.Predicate {
return predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oldPod, oldOk := e.ObjectOld.(*corev1.Pod)
newPod, newOk := e.ObjectNew.(*corev1.Pod)
if !oldOk || !newOk {
return true
}

if oldPod.UID != newPod.UID ||
!equalTime(oldPod.DeletionTimestamp, newPod.DeletionTimestamp) {
return true
}

oldStatus, newStatus := &oldPod.Status, &newPod.Status
if oldStatus.Phase != newStatus.Phase ||
oldStatus.Reason != newStatus.Reason ||
oldStatus.Message != newStatus.Message {
return true
}

if !equality.Semantic.DeepEqual(oldStatus.ContainerStatuses, newStatus.ContainerStatuses) ||
!equality.Semantic.DeepEqual(oldStatus.InitContainerStatuses, newStatus.InitContainerStatuses) {
return true
}

return podReady(oldPod) != podReady(newPod)
},
}
}

// autoscalingListenerOwnedPodPredicate filters updates of the listener pod
// owned by an AutoscalingListener.
//
// The AutoscalingListener reconciler reads the pod's object metadata, because it
// merges labels and annotations back onto the pod and compares the listener
// config resource version annotation, and its whole spec, through
// listenerPodSpecRequiresRecreation. Off the status it reads only the phase,
// reason and message, to detect eviction, and the container statuses, to find
// the listener container and branch on whether it is running or terminated. The
// rest of the pod status is never read: assigned IPs, the node the pod landed
// on, start time, the conditions and the init container statuses.
func autoscalingListenerOwnedPodPredicate() predicate.Predicate {
return predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oldPod, oldOk := e.ObjectOld.(*corev1.Pod)
newPod, newOk := e.ObjectNew.(*corev1.Pod)
if !oldOk || !newOk {
return true
}

if !equalReconciledObjectMeta(&oldPod.ObjectMeta, &newPod.ObjectMeta) ||
!equality.Semantic.DeepEqual(&oldPod.Spec, &newPod.Spec) {
return true
}

oldStatus, newStatus := &oldPod.Status, &newPod.Status
if oldStatus.Phase != newStatus.Phase ||
oldStatus.Reason != newStatus.Reason ||
oldStatus.Message != newStatus.Message {
return true
}

return !equality.Semantic.DeepEqual(oldStatus.ContainerStatuses, newStatus.ContainerStatuses)
},
}
}

// autoscalingListenerOwnedServiceAccountPredicate filters updates of the service
// account owned by an AutoscalingListener.
//
// The AutoscalingListener reconciler only ever reads the service account's
// labels and annotations, which it merges back onto the object. Everything else
// the API server and the token controller write to it, the mounted secrets
// above all, is never read.
func autoscalingListenerOwnedServiceAccountPredicate() predicate.Predicate {
return predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oldServiceAccount, oldOk := e.ObjectOld.(*corev1.ServiceAccount)
newServiceAccount, newOk := e.ObjectNew.(*corev1.ServiceAccount)
if !oldOk || !newOk {
return true
}

return !equalReconciledObjectMeta(&oldServiceAccount.ObjectMeta, &newServiceAccount.ObjectMeta)
},
}
}

// podReady reports whether the pod advertises the Ready condition. It is the
// single source of truth for both the reconciler, which mirrors the result into
// EphemeralRunner.Status.Ready, and the pod predicate, which has to wake the
// reconciler whenever the result changes.
func podReady(pod *corev1.Pod) bool {
var ready bool
var lastTransitionTime time.Time
for _, condition := range pod.Status.Conditions {
if condition.Type == corev1.PodReady && condition.LastTransitionTime.After(lastTransitionTime) {
ready = condition.Status == corev1.ConditionTrue
lastTransitionTime = condition.LastTransitionTime.Time
}
}
return ready
}

// equalReconciledObjectMeta compares the metadata fields the controllers in this
// package branch on. Bookkeeping the API server owns, such as the resource
// version, the managed fields and the timestamps outside of deletion, is
// deliberately left out.
func equalReconciledObjectMeta(old, new *metav1.ObjectMeta) bool {
return old.Generation == new.Generation &&
equalTime(old.DeletionTimestamp, new.DeletionTimestamp) &&
slices.Equal(old.Finalizers, new.Finalizers) &&
equality.Semantic.DeepEqual(old.Labels, new.Labels) &&
equality.Semantic.DeepEqual(old.Annotations, new.Annotations) &&
equality.Semantic.DeepEqual(old.OwnerReferences, new.OwnerReferences)
}

func equalTime(old, new *metav1.Time) bool {
switch {
case old == nil && new == nil:
return true
case old == nil || new == nil:
return false
default:
return old.Equal(new)
}
}
Loading
Loading