@@ -3,6 +3,7 @@ package olm
33import (
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
2324const (
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.
176204func (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