forked from rook/rook
This commit adds context parameter to k8sutil deployment functions. By this, we can handle cancellation during API call of deployment resource. Signed-off-by: Yuichiro Ueno <y1r.ueno@gmail.com>
483 lines
15 KiB
Go
483 lines
15 KiB
Go
/*
|
|
Copyright 2018 The Rook Authors. All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package k8sutil
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
k8stesting "k8s.io/client-go/testing"
|
|
)
|
|
|
|
func TestUpdateMultipleDeploymentsAndWait(t *testing.T) {
|
|
// UpdateMultipleDeployments and WaitForDeploymentsToUpdate are thoroughly tested
|
|
// UpdateMultipleDeploymentsAndWait is a simple wrapper for both of their functionality
|
|
// Merely do basic integration testing on this to ensure they are connected properly
|
|
|
|
namespace := "ns"
|
|
|
|
oldPeriod := waitForDeploymentPeriod
|
|
oldTimeout := waitForDeploymentTimeout
|
|
defer func() {
|
|
waitForDeploymentPeriod = oldPeriod
|
|
waitForDeploymentTimeout = oldTimeout
|
|
}()
|
|
waitForDeploymentPeriod = 1 * time.Millisecond
|
|
waitForDeploymentTimeout = 3 * time.Millisecond
|
|
|
|
// inputs
|
|
var clientset *fake.Clientset
|
|
var deployments []*appsv1.Deployment
|
|
var listFunc func() (*appsv1.DeploymentList, error)
|
|
|
|
// outputs
|
|
var failures Failures
|
|
|
|
// a barebones deployment
|
|
deployment := func(name string) *appsv1.Deployment {
|
|
return &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
}
|
|
}
|
|
// a barebones deployment with some modification
|
|
modifiedDeployment := func(name string) *appsv1.Deployment {
|
|
d := deployment(name)
|
|
d.Spec.Selector = &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{"hi": "mom"},
|
|
}
|
|
return d
|
|
}
|
|
|
|
timesCalled := 0
|
|
// generate a status that is not ready when first called but becomes ready later
|
|
status := func(timesCalled int) appsv1.DeploymentStatus {
|
|
if timesCalled < 0 {
|
|
timesCalled = 0
|
|
}
|
|
return appsv1.DeploymentStatus{
|
|
ObservedGeneration: int64(timesCalled),
|
|
UpdatedReplicas: int32(timesCalled),
|
|
ReadyReplicas: int32(timesCalled),
|
|
Conditions: []appsv1.DeploymentCondition{
|
|
{Type: appsv1.DeploymentProgressing, Reason: "DoinStuff"},
|
|
},
|
|
}
|
|
}
|
|
// generate a deployment with no errors and status=status(timesCalled)
|
|
okayDeployment := func(name string, timesCalled int) appsv1.Deployment {
|
|
return appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Status: status(timesCalled),
|
|
}
|
|
}
|
|
addProgressDeadlineExceeded := func(d *appsv1.Deployment) {
|
|
d.Status.Conditions = append(d.Status.Conditions, appsv1.DeploymentCondition{
|
|
Type: appsv1.DeploymentProgressing, Reason: "ProgressDeadlineExceeded"})
|
|
}
|
|
|
|
clientset = fake.NewSimpleClientset()
|
|
depsUpdated := []string{}
|
|
var deploymentReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
switch action := action.(type) {
|
|
case k8stesting.UpdateActionImpl:
|
|
obj := action.GetObject()
|
|
d, ok := obj.(*appsv1.Deployment)
|
|
if !ok {
|
|
panic("err! action not a deployment")
|
|
}
|
|
depsUpdated = append(depsUpdated, d.Name)
|
|
default:
|
|
panic(fmt.Sprintf("action %v not understood", action))
|
|
}
|
|
return false, nil, nil
|
|
}
|
|
clientset.PrependReactor("update", "deployments", deploymentReactor)
|
|
|
|
t.Run("integration", func(t *testing.T) {
|
|
createDeploymentOrDie(clientset, deployment("d1"))
|
|
createDeploymentOrDie(clientset, deployment("d2"))
|
|
createDeploymentOrDie(clientset, deployment("d3"))
|
|
createDeploymentOrDie(clientset, deployment("d4"))
|
|
deployments = []*appsv1.Deployment{
|
|
modifiedDeployment("d1"), // should be updated (and successfully)
|
|
modifiedDeployment("d2"), // should be updated (but gets progress deadline exceeded)
|
|
modifiedDeployment("d3"), // should be updated (but never becomes ready)
|
|
deployment("d4"), // no changes so should not be updated
|
|
deployment("d5"), // does not exist so should be a failure to update
|
|
}
|
|
listFunc = func() (*appsv1.DeploymentList, error) {
|
|
d2 := okayDeployment("d2", 0)
|
|
if timesCalled >= 1 {
|
|
addProgressDeadlineExceeded(&d2)
|
|
}
|
|
l := &appsv1.DeploymentList{
|
|
Items: []appsv1.Deployment{
|
|
okayDeployment("d1", timesCalled), // becomes ready second time called
|
|
d2,
|
|
okayDeployment("d3", 0), // never becomes ready
|
|
},
|
|
}
|
|
timesCalled++
|
|
return l, nil
|
|
}
|
|
failures = UpdateMultipleDeploymentsAndWait(context.TODO(), clientset, deployments, listFunc)
|
|
assert.Len(t, failures, 3)
|
|
assert.ElementsMatch(t,
|
|
[]string{failures[0].ResourceName, failures[1].ResourceName, failures[2].ResourceName},
|
|
[]string{"d2", "d3", "d5"})
|
|
assert.ElementsMatch(t, depsUpdated, []string{"d1", "d2", "d3"})
|
|
})
|
|
}
|
|
|
|
func TestUpdateMultipleDeployments(t *testing.T) {
|
|
namespace := "ns"
|
|
|
|
// a barebones deployment
|
|
deployment := func(name string, pds *int32) *appsv1.Deployment {
|
|
return &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
ProgressDeadlineSeconds: pds,
|
|
},
|
|
}
|
|
}
|
|
// a barebones deployment with some modification
|
|
modifiedDeployment := func(name string, pds *int32) *appsv1.Deployment {
|
|
d := deployment(name, pds)
|
|
d.Spec.Selector = &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{"hi": "mom"},
|
|
}
|
|
return d
|
|
}
|
|
|
|
// inputs
|
|
var clientset *fake.Clientset
|
|
var deployments []*appsv1.Deployment
|
|
|
|
// outputs
|
|
var deploymentsUpdated DeploymentsUpdated
|
|
var failures Failures
|
|
var pds *int32 // progressDeadlineSeconds
|
|
|
|
t.Run("no deployments to be updated", func(t *testing.T) {
|
|
clientset = fake.NewSimpleClientset()
|
|
deployments = []*appsv1.Deployment{}
|
|
deploymentsUpdated, failures, pds = UpdateMultipleDeployments(context.TODO(), clientset, deployments)
|
|
assert.Len(t, deploymentsUpdated, 0)
|
|
assert.Len(t, failures, 0)
|
|
})
|
|
|
|
t.Run("update deployments successfully", func(t *testing.T) {
|
|
clientset = fake.NewSimpleClientset()
|
|
createDeploymentOrDie(clientset, deployment("d1", nil))
|
|
createDeploymentOrDie(clientset, deployment("d2", nil))
|
|
createDeploymentOrDie(clientset, deployment("d3", nil))
|
|
deployments = []*appsv1.Deployment{
|
|
modifiedDeployment("d1", nil),
|
|
modifiedDeployment("d2", nil),
|
|
modifiedDeployment("d3", nil),
|
|
}
|
|
deploymentsUpdated, failures, pds = UpdateMultipleDeployments(context.TODO(), clientset, deployments)
|
|
assert.Len(t, deploymentsUpdated, 3)
|
|
assert.Len(t, failures, 0)
|
|
assert.Contains(t, deploymentsUpdated, "d1")
|
|
assert.Contains(t, deploymentsUpdated, "d2")
|
|
assert.Contains(t, deploymentsUpdated, "d3")
|
|
assert.Nil(t, pds)
|
|
})
|
|
|
|
t.Run("do not update deployments with no modifications", func(t *testing.T) {
|
|
// clientset with preexisting and updated deployments from previous test
|
|
d3 := modifiedDeployment("d3", newInt32(5))
|
|
// d3.ObjectMeta.Labels = map[string]string{
|
|
// "new": "label",
|
|
// }
|
|
deployments = []*appsv1.Deployment{
|
|
modifiedDeployment("d1", nil), // should not be updated
|
|
// d2 from before should also not be updated
|
|
d3, // should be updated
|
|
}
|
|
deploymentsUpdated, failures, pds = UpdateMultipleDeployments(context.TODO(), clientset, deployments)
|
|
assert.Len(t, deploymentsUpdated, 1)
|
|
assert.Len(t, failures, 0)
|
|
assert.Contains(t, deploymentsUpdated, "d3")
|
|
assert.EqualValues(t, 5, *pds)
|
|
})
|
|
|
|
t.Run("failures if deployments do not exist", func(t *testing.T) {
|
|
clientset = fake.NewSimpleClientset()
|
|
createDeploymentOrDie(clientset, deployment("d1", newInt32(30)))
|
|
createDeploymentOrDie(clientset, deployment("d3", newInt32(30)))
|
|
deployments = []*appsv1.Deployment{
|
|
modifiedDeployment("d1", newInt32(30)),
|
|
modifiedDeployment("d2", newInt32(30)),
|
|
modifiedDeployment("d3", newInt32(30)),
|
|
modifiedDeployment("d4", newInt32(30)),
|
|
}
|
|
deploymentsUpdated, failures, pds = UpdateMultipleDeployments(context.TODO(), clientset, deployments)
|
|
assert.Len(t, deploymentsUpdated, 2)
|
|
assert.Len(t, failures, 2)
|
|
assert.Contains(t, deploymentsUpdated, "d1")
|
|
assert.Contains(t, deploymentsUpdated, "d3")
|
|
assert.ElementsMatch(t, []string{failures[0].ResourceName, failures[1].ResourceName}, []string{"d2", "d4"})
|
|
assert.EqualValues(t, 30, *pds)
|
|
})
|
|
}
|
|
|
|
func TestWaitForDeploymentsToUpdate(t *testing.T) {
|
|
// NOTE: do not actually test progressDeadlineExceeded as an input because we don't want to make
|
|
// this unit test run for multiple seconds.
|
|
|
|
oldPeriod := waitForDeploymentPeriod
|
|
oldTimeout := waitForDeploymentTimeout
|
|
defer func() {
|
|
waitForDeploymentPeriod = oldPeriod
|
|
waitForDeploymentTimeout = oldTimeout
|
|
}()
|
|
waitForDeploymentPeriod = 1 * time.Millisecond
|
|
waitForDeploymentTimeout = 3 * time.Millisecond
|
|
|
|
timesCalled := 0
|
|
// generate a status that is not ready when first called but becomes ready later
|
|
status := func(timesCalled int) appsv1.DeploymentStatus {
|
|
if timesCalled < 0 {
|
|
timesCalled = 0
|
|
}
|
|
return appsv1.DeploymentStatus{
|
|
ObservedGeneration: int64(timesCalled),
|
|
UpdatedReplicas: int32(timesCalled),
|
|
ReadyReplicas: int32(timesCalled),
|
|
Conditions: []appsv1.DeploymentCondition{
|
|
{Type: appsv1.DeploymentProgressing, Reason: "DoinStuff"},
|
|
},
|
|
}
|
|
}
|
|
// generate a deployment with no errors and status=status(timesCalled)
|
|
okayDeployment := func(name string, timesCalled int) appsv1.Deployment {
|
|
return appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Status: status(timesCalled),
|
|
}
|
|
}
|
|
addProgressDeadlineExceeded := func(d *appsv1.Deployment) {
|
|
d.Status.Conditions = append(d.Status.Conditions, appsv1.DeploymentCondition{
|
|
Type: appsv1.DeploymentProgressing, Reason: "ProgressDeadlineExceeded"})
|
|
}
|
|
|
|
// inputs
|
|
var deploymentsUpdated DeploymentsUpdated
|
|
var progressDeadlineSeconds *int32
|
|
var listFunc func() (*appsv1.DeploymentList, error)
|
|
|
|
// outputs
|
|
var failures Failures
|
|
|
|
runTest := func() {
|
|
timesCalled = 0
|
|
failures = WaitForDeploymentsToUpdate(deploymentsUpdated, progressDeadlineSeconds, listFunc)
|
|
}
|
|
|
|
t.Run("not waiting on any deployments", func(t *testing.T) {
|
|
deploymentsUpdated = DeploymentsUpdated{}
|
|
listFunc = func() (*appsv1.DeploymentList, error) {
|
|
l := &appsv1.DeploymentList{
|
|
Items: []appsv1.Deployment{
|
|
okayDeployment("d1", timesCalled), // becomes ready second time called
|
|
okayDeployment("d2", timesCalled-1), // becomes ready third time called
|
|
okayDeployment("d3", timesCalled), // becomes ready second time called but only generation changed on third time
|
|
},
|
|
}
|
|
timesCalled++
|
|
return l, nil
|
|
}
|
|
runTest()
|
|
assert.Len(t, failures, 0)
|
|
})
|
|
|
|
t.Run("wait successful", func(t *testing.T) {
|
|
deploymentsUpdated = DeploymentsUpdated{
|
|
"d1": 0,
|
|
"d2": 0,
|
|
"d3": 1,
|
|
}
|
|
listFunc = func() (*appsv1.DeploymentList, error) {
|
|
l := &appsv1.DeploymentList{
|
|
Items: []appsv1.Deployment{
|
|
okayDeployment("d1", timesCalled), // becomes ready second time called
|
|
okayDeployment("d2", timesCalled-1), // becomes ready third time called
|
|
okayDeployment("d3", timesCalled), // becomes ready second time called but only generation changed on third time
|
|
},
|
|
}
|
|
timesCalled++
|
|
return l, nil
|
|
}
|
|
runTest()
|
|
assert.Len(t, failures, 0)
|
|
})
|
|
|
|
t.Run("some failures", func(t *testing.T) {
|
|
deploymentsUpdated = DeploymentsUpdated{
|
|
"d1": 0,
|
|
"d2": 0,
|
|
"d3": 0,
|
|
}
|
|
listFunc = func() (*appsv1.DeploymentList, error) {
|
|
l := &appsv1.DeploymentList{
|
|
Items: []appsv1.Deployment{
|
|
okayDeployment("d1", timesCalled), // becomes ready second time called
|
|
okayDeployment("d2", 0), // never becomes ready
|
|
okayDeployment("d3", 0), // never becomes ready
|
|
},
|
|
}
|
|
timesCalled++
|
|
return l, nil
|
|
}
|
|
runTest()
|
|
assert.Len(t, failures, 2)
|
|
assert.ElementsMatch(t, []string{failures[0].ResourceName, failures[1].ResourceName}, []string{"d2", "d3"})
|
|
assert.Error(t, failures[0].Error)
|
|
assert.Error(t, failures[1].Error)
|
|
})
|
|
|
|
t.Run("one deployment never listed", func(t *testing.T) {
|
|
deploymentsUpdated = DeploymentsUpdated{
|
|
"d1": 0,
|
|
"d2": 0,
|
|
"d3": 0,
|
|
}
|
|
listFunc = func() (*appsv1.DeploymentList, error) {
|
|
l := &appsv1.DeploymentList{
|
|
Items: []appsv1.Deployment{
|
|
okayDeployment("d1", timesCalled), // becomes ready second time called
|
|
okayDeployment("d2", timesCalled-1), // becomes ready third time called
|
|
},
|
|
}
|
|
timesCalled++
|
|
return l, nil
|
|
}
|
|
runTest()
|
|
assert.Len(t, failures, 1)
|
|
assert.ElementsMatch(t, []string{failures[0].ResourceName}, []string{"d3"})
|
|
assert.Error(t, failures[0].Error)
|
|
})
|
|
|
|
t.Run("fail to list deployments", func(t *testing.T) {
|
|
deploymentsUpdated = DeploymentsUpdated{
|
|
"d1": 0,
|
|
"d2": 0,
|
|
"d3": 0,
|
|
}
|
|
listFunc = func() (*appsv1.DeploymentList, error) {
|
|
return nil, fmt.Errorf("fake error listing deployments")
|
|
}
|
|
runTest()
|
|
assert.Len(t, failures, 3)
|
|
assert.ElementsMatch(t,
|
|
[]string{failures[0].ResourceName, failures[1].ResourceName, failures[2].ResourceName},
|
|
[]string{"d1", "d2", "d3"})
|
|
assert.Error(t, failures[0].Error)
|
|
assert.Error(t, failures[1].Error)
|
|
assert.Error(t, failures[2].Error)
|
|
})
|
|
|
|
t.Run("deployment progress deadline exceeded", func(t *testing.T) {
|
|
deploymentsUpdated = DeploymentsUpdated{
|
|
"d1": 0,
|
|
"d2": 0,
|
|
"d3": 0,
|
|
}
|
|
listFunc = func() (*appsv1.DeploymentList, error) {
|
|
d2 := okayDeployment("d2", 0)
|
|
d3 := okayDeployment("d3", 0)
|
|
if timesCalled > 2 {
|
|
addProgressDeadlineExceeded(&d2)
|
|
addProgressDeadlineExceeded(&d3)
|
|
}
|
|
l := &appsv1.DeploymentList{
|
|
Items: []appsv1.Deployment{
|
|
okayDeployment("d1", timesCalled), // becomes ready second time called
|
|
d2,
|
|
d3,
|
|
},
|
|
}
|
|
timesCalled++
|
|
return l, nil
|
|
}
|
|
runTest()
|
|
assert.Len(t, failures, 2)
|
|
assert.ElementsMatch(t, []string{failures[0].ResourceName, failures[1].ResourceName}, []string{"d2", "d3"})
|
|
assert.Error(t, failures[0].Error)
|
|
assert.Error(t, failures[1].Error)
|
|
})
|
|
}
|
|
|
|
func Test_maxInt32Ptr(t *testing.T) {
|
|
t.Run("both nil", func(t *testing.T) {
|
|
assert.Nil(t, maxInt32Ptr(nil, nil))
|
|
})
|
|
|
|
type args struct {
|
|
a *int32
|
|
b *int32
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want int32
|
|
}{
|
|
{"a nil", args{nil, newInt32(2)}, 2},
|
|
{"b nil", args{newInt32(0), nil}, 0},
|
|
{"negatives", args{newInt32(-5), newInt32(-4)}, -4},
|
|
{"neg and pos", args{newInt32(-3), newInt32(1)}, 1},
|
|
{"positives", args{newInt32(1), newInt32(3)}, 3},
|
|
{"with zero", args{newInt32(0), newInt32(2)}, 2},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, *maxInt32Ptr(tt.args.a, tt.args.b))
|
|
})
|
|
}
|
|
}
|
|
|
|
func newInt32(i int32) *int32 {
|
|
return &i
|
|
}
|
|
|
|
func createDeploymentOrDie(clientset *fake.Clientset, d *appsv1.Deployment) {
|
|
_, err := clientset.AppsV1().Deployments(d.Namespace).Create(context.TODO(), d, metav1.CreateOptions{})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|