forked from rook/rook
The ci was using a pretty old version og golangci-lint. This updates to the latest version. Additionally, it silences some gosec integer conversion overflow false positves and fixes some real errors of this category and string format errors found by golangci-lint, while at it. Co-authored-by: Blaine Gardner <b.blaine.gardner@gmail.com> Co-authored-by: Travis Nielsen <tnielsen@redhat.com> Signed-off-by: Michael Adam <obnox@samba.org>
487 lines
15 KiB
Go
487 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 := int32(0)
|
|
// generate a status that is not ready when first called but becomes ready later
|
|
status := func(timesCalled int32) appsv1.DeploymentStatus {
|
|
if timesCalled < 0 {
|
|
timesCalled = 0
|
|
}
|
|
return appsv1.DeploymentStatus{
|
|
ObservedGeneration: int64(timesCalled),
|
|
//nolint:gosec // G115 widening cast - no integer overflow
|
|
UpdatedReplicas: int32(timesCalled),
|
|
//nolint:gosec // G115 widening cast - no integer overflow
|
|
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 int32) 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),
|
|
// nolint:gosec // G115 No overflow: widening cast.
|
|
UpdatedReplicas: int32(timesCalled),
|
|
// nolint:gosec // G115 No overflow: widening cast.
|
|
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)
|
|
}
|
|
}
|