k8s.io/kubernetes@v1.29.3/pkg/controller/deployment/progress_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package deployment
    18  
    19  import (
    20  	"context"
    21  	"math"
    22  	"testing"
    23  	"time"
    24  
    25  	apps "k8s.io/api/apps/v1"
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/client-go/kubernetes/fake"
    29  	"k8s.io/client-go/util/workqueue"
    30  	"k8s.io/klog/v2/ktesting"
    31  	"k8s.io/kubernetes/pkg/controller/deployment/util"
    32  )
    33  
    34  func newDeploymentStatus(replicas, updatedReplicas, availableReplicas int32) apps.DeploymentStatus {
    35  	return apps.DeploymentStatus{
    36  		Replicas:          replicas,
    37  		UpdatedReplicas:   updatedReplicas,
    38  		AvailableReplicas: availableReplicas,
    39  	}
    40  }
    41  
    42  // assumes the retuned deployment is always observed - not needed to be tested here.
    43  func currentDeployment(pds *int32, replicas, statusReplicas, updatedReplicas, availableReplicas int32, conditions []apps.DeploymentCondition) *apps.Deployment {
    44  	d := &apps.Deployment{
    45  		ObjectMeta: metav1.ObjectMeta{
    46  			Name: "progress-test",
    47  		},
    48  		Spec: apps.DeploymentSpec{
    49  			ProgressDeadlineSeconds: pds,
    50  			Replicas:                &replicas,
    51  			Strategy: apps.DeploymentStrategy{
    52  				Type: apps.RecreateDeploymentStrategyType,
    53  			},
    54  		},
    55  		Status: newDeploymentStatus(statusReplicas, updatedReplicas, availableReplicas),
    56  	}
    57  	d.Status.Conditions = conditions
    58  	return d
    59  }
    60  
    61  // helper to create RS with given availableReplicas
    62  func newRSWithAvailable(name string, specReplicas, statusReplicas, availableReplicas int32) *apps.ReplicaSet {
    63  	rs := rs(name, specReplicas, nil, metav1.Time{})
    64  	rs.Status = apps.ReplicaSetStatus{
    65  		Replicas:          statusReplicas,
    66  		AvailableReplicas: availableReplicas,
    67  	}
    68  	return rs
    69  }
    70  
    71  func TestRequeueStuckDeployment(t *testing.T) {
    72  	pds := int32(60)
    73  	infinite := int32(math.MaxInt32)
    74  	failed := []apps.DeploymentCondition{
    75  		{
    76  			Type:   apps.DeploymentProgressing,
    77  			Status: v1.ConditionFalse,
    78  			Reason: util.TimedOutReason,
    79  		},
    80  	}
    81  	stuck := []apps.DeploymentCondition{
    82  		{
    83  			Type:           apps.DeploymentProgressing,
    84  			Status:         v1.ConditionTrue,
    85  			LastUpdateTime: metav1.Date(2017, 2, 15, 18, 49, 00, 00, time.UTC),
    86  		},
    87  	}
    88  
    89  	tests := []struct {
    90  		name     string
    91  		d        *apps.Deployment
    92  		status   apps.DeploymentStatus
    93  		nowFn    func() time.Time
    94  		expected time.Duration
    95  	}{
    96  		{
    97  			name:     "nil progressDeadlineSeconds specified",
    98  			d:        currentDeployment(nil, 4, 3, 3, 2, nil),
    99  			status:   newDeploymentStatus(3, 3, 2),
   100  			expected: time.Duration(-1),
   101  		},
   102  		{
   103  			name:     "infinite progressDeadlineSeconds specified",
   104  			d:        currentDeployment(&infinite, 4, 3, 3, 2, nil),
   105  			status:   newDeploymentStatus(3, 3, 2),
   106  			expected: time.Duration(-1),
   107  		},
   108  		{
   109  			name:     "no progressing condition found",
   110  			d:        currentDeployment(&pds, 4, 3, 3, 2, nil),
   111  			status:   newDeploymentStatus(3, 3, 2),
   112  			expected: time.Duration(-1),
   113  		},
   114  		{
   115  			name:     "complete deployment does not need to be requeued",
   116  			d:        currentDeployment(&pds, 3, 3, 3, 3, nil),
   117  			status:   newDeploymentStatus(3, 3, 3),
   118  			expected: time.Duration(-1),
   119  		},
   120  		{
   121  			name:     "already failed deployment does not need to be requeued",
   122  			d:        currentDeployment(&pds, 3, 3, 3, 0, failed),
   123  			status:   newDeploymentStatus(3, 3, 0),
   124  			expected: time.Duration(-1),
   125  		},
   126  		{
   127  			name:     "stuck deployment - 30s",
   128  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   129  			status:   newDeploymentStatus(3, 3, 1),
   130  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 49, 30, 00, time.UTC).Time },
   131  			expected: 30 * time.Second,
   132  		},
   133  		{
   134  			name:     "stuck deployment - 1s",
   135  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   136  			status:   newDeploymentStatus(3, 3, 1),
   137  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 49, 59, 00, time.UTC).Time },
   138  			expected: 1 * time.Second,
   139  		},
   140  		{
   141  			name:     "failed deployment - less than a second => now",
   142  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   143  			status:   newDeploymentStatus(3, 3, 1),
   144  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 49, 59, 1, time.UTC).Time },
   145  			expected: time.Duration(0),
   146  		},
   147  		{
   148  			name:     "failed deployment - now",
   149  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   150  			status:   newDeploymentStatus(3, 3, 1),
   151  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 50, 00, 00, time.UTC).Time },
   152  			expected: time.Duration(0),
   153  		},
   154  		{
   155  			name:     "failed deployment - 1s after deadline",
   156  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   157  			status:   newDeploymentStatus(3, 3, 1),
   158  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 50, 01, 00, time.UTC).Time },
   159  			expected: time.Duration(0),
   160  		},
   161  		{
   162  			name:     "failed deployment - 60s after deadline",
   163  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   164  			status:   newDeploymentStatus(3, 3, 1),
   165  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 51, 00, 00, time.UTC).Time },
   166  			expected: time.Duration(0),
   167  		},
   168  	}
   169  
   170  	dc := &DeploymentController{
   171  		queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "doesnt_matter"),
   172  	}
   173  	dc.enqueueDeployment = dc.enqueue
   174  
   175  	for _, test := range tests {
   176  		t.Run(test.name, func(t *testing.T) {
   177  			if test.nowFn != nil {
   178  				nowFn = test.nowFn
   179  			}
   180  			_, ctx := ktesting.NewTestContext(t)
   181  			ctx, cancel := context.WithCancel(ctx)
   182  			defer cancel()
   183  			got := dc.requeueStuckDeployment(ctx, test.d, test.status)
   184  			if got != test.expected {
   185  				t.Errorf("%s: got duration: %v, expected duration: %v", test.name, got, test.expected)
   186  			}
   187  		})
   188  	}
   189  }
   190  
   191  func TestSyncRolloutStatus(t *testing.T) {
   192  	pds := int32(60)
   193  	testTime := metav1.Date(2017, 2, 15, 18, 49, 00, 00, time.UTC)
   194  	failedTimedOut := apps.DeploymentCondition{
   195  		Type:   apps.DeploymentProgressing,
   196  		Status: v1.ConditionFalse,
   197  		Reason: util.TimedOutReason,
   198  	}
   199  	newRSAvailable := apps.DeploymentCondition{
   200  		Type:               apps.DeploymentProgressing,
   201  		Status:             v1.ConditionTrue,
   202  		Reason:             util.NewRSAvailableReason,
   203  		LastUpdateTime:     testTime,
   204  		LastTransitionTime: testTime,
   205  	}
   206  	replicaSetUpdated := apps.DeploymentCondition{
   207  		Type:               apps.DeploymentProgressing,
   208  		Status:             v1.ConditionTrue,
   209  		Reason:             util.ReplicaSetUpdatedReason,
   210  		LastUpdateTime:     testTime,
   211  		LastTransitionTime: testTime,
   212  	}
   213  
   214  	tests := []struct {
   215  		name            string
   216  		d               *apps.Deployment
   217  		allRSs          []*apps.ReplicaSet
   218  		newRS           *apps.ReplicaSet
   219  		conditionType   apps.DeploymentConditionType
   220  		conditionStatus v1.ConditionStatus
   221  		conditionReason string
   222  		lastUpdate      metav1.Time
   223  		lastTransition  metav1.Time
   224  	}{
   225  		{
   226  			name:   "General: remove Progressing condition and do not estimate progress if deployment has no Progress Deadline",
   227  			d:      currentDeployment(nil, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}),
   228  			allRSs: []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)},
   229  			newRS:  newRSWithAvailable("foo", 3, 2, 2),
   230  		},
   231  		{
   232  			name:            "General: do not estimate progress of deployment with only one active ReplicaSet",
   233  			d:               currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{newRSAvailable}),
   234  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("bar", 3, 3, 3)},
   235  			conditionType:   apps.DeploymentProgressing,
   236  			conditionStatus: v1.ConditionTrue,
   237  			conditionReason: util.NewRSAvailableReason,
   238  			lastUpdate:      testTime,
   239  			lastTransition:  testTime,
   240  		},
   241  		{
   242  			name:            "DeploymentProgressing: dont update lastTransitionTime if deployment already has Progressing=True",
   243  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}),
   244  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)},
   245  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   246  			conditionType:   apps.DeploymentProgressing,
   247  			conditionStatus: v1.ConditionTrue,
   248  			conditionReason: util.ReplicaSetUpdatedReason,
   249  			lastTransition:  testTime,
   250  		},
   251  		{
   252  			name:            "DeploymentProgressing: update everything if deployment has Progressing=False",
   253  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{failedTimedOut}),
   254  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)},
   255  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   256  			conditionType:   apps.DeploymentProgressing,
   257  			conditionStatus: v1.ConditionTrue,
   258  			conditionReason: util.ReplicaSetUpdatedReason,
   259  		},
   260  		{
   261  			name:            "DeploymentProgressing: create Progressing condition if it does not exist",
   262  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{}),
   263  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)},
   264  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   265  			conditionType:   apps.DeploymentProgressing,
   266  			conditionStatus: v1.ConditionTrue,
   267  			conditionReason: util.ReplicaSetUpdatedReason,
   268  		},
   269  		{
   270  			name:            "DeploymentComplete: dont update lastTransitionTime if deployment already has Progressing=True",
   271  			d:               currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{replicaSetUpdated}),
   272  			allRSs:          []*apps.ReplicaSet{},
   273  			newRS:           newRSWithAvailable("foo", 3, 3, 3),
   274  			conditionType:   apps.DeploymentProgressing,
   275  			conditionStatus: v1.ConditionTrue,
   276  			conditionReason: util.NewRSAvailableReason,
   277  			lastTransition:  testTime,
   278  		},
   279  		{
   280  			name:            "DeploymentComplete: update everything if deployment has Progressing=False",
   281  			d:               currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{failedTimedOut}),
   282  			allRSs:          []*apps.ReplicaSet{},
   283  			newRS:           newRSWithAvailable("foo", 3, 3, 3),
   284  			conditionType:   apps.DeploymentProgressing,
   285  			conditionStatus: v1.ConditionTrue,
   286  			conditionReason: util.NewRSAvailableReason,
   287  		},
   288  		{
   289  			name:            "DeploymentComplete: create Progressing condition if it does not exist",
   290  			d:               currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{}),
   291  			allRSs:          []*apps.ReplicaSet{},
   292  			newRS:           newRSWithAvailable("foo", 3, 3, 3),
   293  			conditionType:   apps.DeploymentProgressing,
   294  			conditionStatus: v1.ConditionTrue,
   295  			conditionReason: util.NewRSAvailableReason,
   296  		},
   297  		{
   298  			name:            "DeploymentComplete: defend against NPE when newRS=nil",
   299  			d:               currentDeployment(&pds, 0, 3, 3, 3, []apps.DeploymentCondition{replicaSetUpdated}),
   300  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("foo", 0, 0, 0)},
   301  			conditionType:   apps.DeploymentProgressing,
   302  			conditionStatus: v1.ConditionTrue,
   303  			conditionReason: util.NewRSAvailableReason,
   304  		},
   305  		{
   306  			name:            "DeploymentTimedOut: update status if rollout exceeds Progress Deadline",
   307  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}),
   308  			allRSs:          []*apps.ReplicaSet{},
   309  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   310  			conditionType:   apps.DeploymentProgressing,
   311  			conditionStatus: v1.ConditionFalse,
   312  			conditionReason: util.TimedOutReason,
   313  		},
   314  		{
   315  			name:            "DeploymentTimedOut: do not update status if deployment has existing timedOut condition",
   316  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{failedTimedOut}),
   317  			allRSs:          []*apps.ReplicaSet{},
   318  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   319  			conditionType:   apps.DeploymentProgressing,
   320  			conditionStatus: v1.ConditionFalse,
   321  			conditionReason: util.TimedOutReason,
   322  			lastUpdate:      testTime,
   323  			lastTransition:  testTime,
   324  		},
   325  	}
   326  
   327  	for _, test := range tests {
   328  		t.Run(test.name, func(t *testing.T) {
   329  			fake := fake.Clientset{}
   330  			dc := &DeploymentController{
   331  				client: &fake,
   332  			}
   333  
   334  			if test.newRS != nil {
   335  				test.allRSs = append(test.allRSs, test.newRS)
   336  			}
   337  			_, ctx := ktesting.NewTestContext(t)
   338  			err := dc.syncRolloutStatus(ctx, test.allRSs, test.newRS, test.d)
   339  			if err != nil {
   340  				t.Error(err)
   341  			}
   342  
   343  			newCond := util.GetDeploymentCondition(test.d.Status, test.conditionType)
   344  			switch {
   345  			case newCond == nil:
   346  				if test.d.Spec.ProgressDeadlineSeconds != nil && *test.d.Spec.ProgressDeadlineSeconds != math.MaxInt32 {
   347  					t.Errorf("%s: expected deployment condition: %s", test.name, test.conditionType)
   348  				}
   349  			case newCond.Status != test.conditionStatus || newCond.Reason != test.conditionReason:
   350  				t.Errorf("%s: DeploymentProgressing has status %s with reason %s. Expected %s with %s.", test.name, newCond.Status, newCond.Reason, test.conditionStatus, test.conditionReason)
   351  			case !test.lastUpdate.IsZero() && test.lastUpdate != testTime:
   352  				t.Errorf("%s: LastUpdateTime was changed to %s but expected %s;", test.name, test.lastUpdate, testTime)
   353  			case !test.lastTransition.IsZero() && test.lastTransition != testTime:
   354  				t.Errorf("%s: LastTransitionTime was changed to %s but expected %s;", test.name, test.lastTransition, testTime)
   355  			}
   356  		})
   357  	}
   358  }