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) } }