Skip to content

Commit af924b7

Browse files
Review - Suggestions
Assisted-by: Cursor
1 parent e67282f commit af924b7

File tree

3 files changed

+755
-180
lines changed

3 files changed

+755
-180
lines changed

pkg/controller/operators/olm/apiservices.go

Lines changed: 171 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package olm
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
hashutil "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/hash"
89
log "github.com/sirupsen/logrus"
@@ -21,10 +22,36 @@ import (
2122
)
2223

2324
const (
24-
// Name of packageserver API service
25+
// Name of packageserver API service.
2526
PackageserverName = "v1.packages.operators.coreos.com"
27+
28+
// expectedDisruptionGracePeriod is how long we still consider pod lifecycle churn
29+
// (terminating pods, new containers starting, node drains) to be "expected" before
30+
// we decide the outage is probably a real failure.
31+
expectedDisruptionGracePeriod = 3 * time.Minute
32+
33+
// retryableAPIServiceRequeueDelay throttles how often we retry while we wait for
34+
// the backing pods to come back. This keeps us from hot-looping during a long drain.
35+
retryableAPIServiceRequeueDelay = 15 * time.Second
2636
)
2737

38+
var expectedPodWaitingReasons = map[string]struct{}{
39+
// Pods report these reasons while new containers are coming up.
40+
"ContainerCreating": {},
41+
"PodInitializing": {},
42+
}
43+
44+
var expectedPodStatusReasons = map[string]struct{}{
45+
// NodeShutdown is set while the kubelet gracefully evicts workloads during a reboot.
46+
"NodeShutdown": {},
47+
}
48+
49+
var expectedPodScheduledReasons = map[string]struct{}{
50+
// These scheduler reasons indicate deletion or node drain rather than a placement failure.
51+
"Terminating": {},
52+
"NodeShutdown": {},
53+
}
54+
2855
// apiServiceResourceErrorActionable returns true if OLM can do something about any one
2956
// of the apiService errors in errs; otherwise returns false
3057
//
@@ -171,13 +198,10 @@ func (a *Operator) checkAPIServiceResources(csv *v1alpha1.ClusterServiceVersion,
171198

172199
// isAPIServiceBackendDisrupted checks if the APIService is unavailable due to expected pod disruption
173200
// (e.g., during node reboot or cluster upgrade) rather than an actual failure.
174-
// According to the Progressing condition contract, operators should not report Progressing=True
175-
// only because pods are adjusting to new nodes or rebooting during cluster upgrade.
201+
// According to the Progressing condition contract, operators should stay quiet while we reconcile
202+
// to a previously healthy state (like pods rolling on new nodes), so we use this check to spot
203+
// those short-lived blips.
176204
func (a *Operator) isAPIServiceBackendDisrupted(csv *v1alpha1.ClusterServiceVersion, apiServiceName string) bool {
177-
// Get the deployment that backs this APIService
178-
// For most APIServices, the deployment name matches the CSV name or is specified in the CSV
179-
180-
// Try to find the deployment from the CSV's install strategy
181205
strategy, err := a.resolver.UnmarshalStrategy(csv.Spec.InstallStrategy)
182206
if err != nil {
183207
a.logger.Debugf("Unable to unmarshal strategy for CSV %s: %v", csv.Name, err)
@@ -190,59 +214,164 @@ func (a *Operator) isAPIServiceBackendDisrupted(csv *v1alpha1.ClusterServiceVers
190214
return false
191215
}
192216

193-
// Check each deployment's pods
217+
// Map the APIService back to the deployment(s) that serve it so we ignore unrelated rollouts.
218+
targetDeploymentNames := make(map[string]struct{})
219+
for _, desc := range csv.Spec.APIServiceDefinitions.Owned {
220+
if desc.GetName() == apiServiceName && desc.DeploymentName != "" {
221+
targetDeploymentNames[desc.DeploymentName] = struct{}{}
222+
}
223+
}
224+
225+
if len(targetDeploymentNames) == 0 {
226+
a.logger.Debugf("APIService %s does not declare a backing deployment", apiServiceName)
227+
return false
228+
}
229+
194230
for _, deploymentSpec := range strategyDetailsDeployment.DeploymentSpecs {
231+
if _, ok := targetDeploymentNames[deploymentSpec.Name]; !ok {
232+
continue
233+
}
234+
195235
deployment, err := a.lister.AppsV1().DeploymentLister().Deployments(csv.Namespace).Get(deploymentSpec.Name)
196236
if err != nil {
197237
if apierrors.IsNotFound(err) {
238+
a.logger.Debugf("Deployment %s for APIService %s not found", deploymentSpec.Name, apiServiceName)
198239
continue
199240
}
200241
a.logger.Debugf("Error getting deployment %s: %v", deploymentSpec.Name, err)
201242
continue
202243
}
203244

204-
// Check if deployment is being updated or rolling out
205-
if deployment.Status.UnavailableReplicas > 0 ||
206-
deployment.Status.UpdatedReplicas < deployment.Status.Replicas {
207-
a.logger.Debugf("Deployment %s has unavailable replicas, likely due to pod disruption", deploymentSpec.Name)
245+
pods, err := a.podsForDeployment(csv.Namespace, deployment)
246+
if err != nil {
247+
a.logger.Debugf("Error listing pods for deployment %s: %v", deploymentSpec.Name, err)
248+
continue
249+
}
250+
251+
if deploymentExperiencingExpectedDisruption(deployment, pods, a.clock.Now(), expectedDisruptionGracePeriod) {
252+
a.logger.Debugf("Deployment %s backing APIService %s is experiencing expected disruption", deploymentSpec.Name, apiServiceName)
253+
return true
254+
}
255+
}
208256

209-
// Check pod status to confirm disruption
210-
selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)
211-
if err != nil {
212-
a.logger.Debugf("Error parsing deployment selector: %v", err)
213-
continue
214-
}
257+
return false
258+
}
215259

216-
pods, err := a.lister.CoreV1().PodLister().Pods(csv.Namespace).List(selector)
217-
if err != nil {
218-
a.logger.Debugf("Error listing pods: %v", err)
219-
continue
260+
func (a *Operator) podsForDeployment(namespace string, deployment *appsv1.Deployment) ([]*corev1.Pod, error) {
261+
if deployment == nil || deployment.Spec.Selector == nil {
262+
// Without a selector there is no easy way to find related pods, so bail out.
263+
return nil, fmt.Errorf("deployment %s/%s missing selector", namespace, deployment.GetName())
264+
}
265+
266+
selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)
267+
if err != nil {
268+
return nil, err
269+
}
270+
271+
return a.lister.CoreV1().PodLister().Pods(namespace).List(selector)
272+
}
273+
274+
// deploymentExperiencingExpectedDisruption returns true when the deployment looks unhealthy
275+
// but everything we can observe points to a short-lived disruption (e.g. pods draining for a reboot).
276+
func deploymentExperiencingExpectedDisruption(deployment *appsv1.Deployment, pods []*corev1.Pod, now time.Time, gracePeriod time.Duration) bool {
277+
if deployment == nil {
278+
return false
279+
}
280+
281+
if deployment.Status.UnavailableReplicas == 0 {
282+
return false
283+
}
284+
285+
if len(pods) == 0 {
286+
return deploymentRecentlyProgressing(deployment, now, gracePeriod)
287+
}
288+
289+
for _, pod := range pods {
290+
if isPodExpectedDisruption(pod, now, gracePeriod) {
291+
return true
292+
}
293+
}
294+
295+
return false
296+
}
297+
298+
func isPodExpectedDisruption(pod *corev1.Pod, now time.Time, gracePeriod time.Duration) bool {
299+
if pod == nil {
300+
return false
301+
}
302+
303+
if pod.DeletionTimestamp != nil {
304+
// Pods carry a deletion timestamp as soon as eviction starts. Give them a little time to finish draining.
305+
return now.Sub(pod.DeletionTimestamp.Time) <= gracePeriod
306+
}
307+
308+
if _, ok := expectedPodStatusReasons[pod.Status.Reason]; ok {
309+
// NodeShutdown shows up while a node is rebooting. Allow one grace window from when the pod last ran.
310+
reference := pod.Status.StartTime
311+
if reference == nil {
312+
reference = &pod.ObjectMeta.CreationTimestamp
313+
}
314+
if reference != nil && !reference.IsZero() {
315+
return now.Sub(reference.Time) <= gracePeriod
316+
}
317+
return true
318+
}
319+
320+
for _, cond := range pod.Status.Conditions {
321+
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionFalse && cond.Reason == "Terminating" {
322+
if cond.LastTransitionTime.IsZero() {
323+
return true
220324
}
325+
return now.Sub(cond.LastTransitionTime.Time) <= gracePeriod
326+
}
221327

222-
// Check if any pod is in Terminating or ContainerCreating state
223-
for _, pod := range pods {
224-
// Pod is terminating (DeletionTimestamp is set)
225-
if pod.DeletionTimestamp != nil {
226-
a.logger.Debugf("Pod %s is terminating - expected disruption", pod.Name)
328+
if cond.Type == corev1.PodScheduled && cond.Status == corev1.ConditionFalse {
329+
if _, ok := expectedPodScheduledReasons[cond.Reason]; ok {
330+
if cond.LastTransitionTime.IsZero() {
227331
return true
228332
}
333+
return now.Sub(cond.LastTransitionTime.Time) <= gracePeriod
334+
}
335+
}
336+
}
229337

230-
// Pod is pending (being scheduled/created)
231-
if pod.Status.Phase == corev1.PodPending {
232-
a.logger.Debugf("Pod %s is pending - expected disruption", pod.Name)
233-
return true
234-
}
338+
for _, status := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) {
339+
if status.State.Waiting == nil {
340+
continue
341+
}
342+
if _, ok := expectedPodWaitingReasons[status.State.Waiting.Reason]; ok {
343+
reference := pod.Status.StartTime
344+
if reference == nil || reference.IsZero() {
345+
reference = &pod.ObjectMeta.CreationTimestamp
346+
}
347+
if reference != nil && !reference.IsZero() {
348+
return now.Sub(reference.Time) <= gracePeriod
349+
}
350+
return true
351+
}
352+
}
235353

236-
// Check container statuses for restarting containers
237-
for _, containerStatus := range pod.Status.ContainerStatuses {
238-
if containerStatus.State.Waiting != nil {
239-
reason := containerStatus.State.Waiting.Reason
240-
if reason == "ContainerCreating" || reason == "PodInitializing" {
241-
a.logger.Debugf("Pod %s container is starting - expected disruption", pod.Name)
242-
return true
243-
}
244-
}
245-
}
354+
return false
355+
}
356+
357+
// deploymentRecentlyProgressing is a fallback for when we cannot find any pods. If the deployment
358+
// just reported progress we assume the kubelet is still spinning up new replicas.
359+
func deploymentRecentlyProgressing(deployment *appsv1.Deployment, now time.Time, gracePeriod time.Duration) bool {
360+
if deployment == nil {
361+
return false
362+
}
363+
364+
for _, cond := range deployment.Status.Conditions {
365+
if cond.Type != appsv1.DeploymentProgressing || cond.Status != corev1.ConditionTrue {
366+
continue
367+
}
368+
switch cond.Reason {
369+
case "NewReplicaSetAvailable", "ReplicaSetUpdated", "ScalingReplicaSet":
370+
if cond.LastUpdateTime.IsZero() {
371+
return true
372+
}
373+
if now.Sub(cond.LastUpdateTime.Time) <= gracePeriod {
374+
return true
246375
}
247376
}
248377
}

0 commit comments

Comments
 (0)