Files
my-rook-config/pkg/operator/k8sutil/deployment_test.go
T
df511fb58f ci: update golangci-lint to the latest version (v1.62)
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>
2024-12-14 14:47:30 +01:00

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)
}
}