mirror of
https://github.com/actions-runner-controller/actions-runner-controller.git
synced 2026-09-30 17:30:45 +02:00
Filter owned-resource events in the workqueue (#4647)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot App
parent
9ce3169df3
commit
ff8bfdee15
@@ -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
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user