Files
kubernetes/cmd/kubeadm/app/util/apiclient/idempotency_test.go
Stephen Kitt efdb735750 kubeadm: fix idempotency retain, address deprecation
CreateOrRetain is supposed to operate on an object name which isn't
necessarily the given object's name (for use in migrations), this
restores that feature.

Replace all uses of deprecated functions with their generic variants.

Providing the context externally isn't useful right now, drop it from
the new functions and use context.Background() where needed.

Signed-off-by: Stephen Kitt <skitt@redhat.com>
2024-12-19 14:29:09 +01:00

579 lines
18 KiB
Go

/*
Copyright 2018 The Kubernetes Authors.
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 apiclient
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
clientsetfake "k8s.io/client-go/kubernetes/fake"
clientgotesting "k8s.io/client-go/testing"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
func TestMain(m *testing.M) {
// Override the default interval and timeouts during tests
defaultRetryInterval := apiCallRetryInterval
apiCallRetryInterval = time.Millisecond * 50
defaultTimeouts := kubeadmapi.GetActiveTimeouts()
defaultAPICallTimeout := defaultTimeouts.KubernetesAPICall
defaultTimeouts.KubernetesAPICall = &metav1.Duration{Duration: apiCallRetryInterval}
exitVal := m.Run()
// Restore the default interval and timeouts
apiCallRetryInterval = defaultRetryInterval
defaultTimeouts.KubernetesAPICall = defaultAPICallTimeout
os.Exit(exitVal)
}
func testCreateOrUpdate[T kubernetesObject](t *testing.T, resource, resources string, empty T, clientBuilder func(kubernetes.Interface, T) kubernetesInterface[T]) {
tests := []struct {
nameFormat string
setupClient func(*clientsetfake.Clientset, string)
expectedError bool
}{
{
nameFormat: "create %s success",
setupClient: func(client *clientsetfake.Clientset, resources string) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
},
expectedError: false,
},
{
nameFormat: "create %s returns error",
setupClient: func(client *clientsetfake.Clientset, resources string) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("unknown error")
})
},
expectedError: true,
},
{
nameFormat: "%s exists, update it",
setupClient: func(client *clientsetfake.Clientset, resources string) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewAlreadyExists(schema.GroupResource{}, "name")
})
client.PrependReactor("update", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
},
expectedError: false,
},
{
nameFormat: "%s exists, update error",
setupClient: func(client *clientsetfake.Clientset, resources string) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewAlreadyExists(schema.GroupResource{}, "name")
})
client.PrependReactor("update", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("")
})
},
expectedError: true,
},
}
for _, tc := range tests {
t.Run(fmt.Sprintf(tc.nameFormat, resource), func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
tc.setupClient(client, resources)
err := CreateOrUpdate(clientBuilder(client, empty), empty)
if (err != nil) != tc.expectedError {
t.Fatalf("expected error: %v, got %v, error: %v", tc.expectedError, err != nil, err)
}
})
}
}
func TestCreateOrUpdateConfigMap(t *testing.T) {
testCreateOrUpdate(t, "configmap", "configmaps", &v1.ConfigMap{},
func(client kubernetes.Interface, obj *v1.ConfigMap) kubernetesInterface[*v1.ConfigMap] {
return client.CoreV1().ConfigMaps(obj.ObjectMeta.Namespace)
})
}
func testCreateOrMutate[T kubernetesObject](t *testing.T, resource, resources string, empty T, clientBuilder func(kubernetes.Interface, T) kubernetesInterface[T]) {
tests := []struct {
nameFormat string
setupClient func(*clientsetfake.Clientset)
mutator objectMutator[T]
expectedError bool
}{
{
nameFormat: "create %s",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
client.PrependReactor("get", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
client.PrependReactor("update", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
},
expectedError: false,
},
{
nameFormat: "create %s error",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("")
})
},
expectedError: true,
},
{
nameFormat: "%s exists, mutate returns error",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewAlreadyExists(schema.GroupResource{}, "name")
})
client.PrependReactor("get", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, empty, nil
})
},
mutator: func(T) error { return errors.New("") },
expectedError: true,
},
{
nameFormat: "%s exists, get returns error",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewAlreadyExists(schema.GroupResource{}, "name")
})
client.PrependReactor("get", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("")
})
},
expectedError: true,
},
{
nameFormat: "%s exists, mutate returns error",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewAlreadyExists(schema.GroupResource{}, "name")
})
client.PrependReactor("get", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, empty, nil
})
client.PrependReactor("update", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("")
})
},
mutator: func(T) error { return nil },
expectedError: true,
},
}
for _, tc := range tests {
t.Run(fmt.Sprintf(tc.nameFormat, resource), func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
tc.setupClient(client)
err := CreateOrMutate[T](clientBuilder(client, empty), empty, tc.mutator)
if (err != nil) != tc.expectedError {
t.Fatalf("expected error: %v, got %v, error: %v", tc.expectedError, err != nil, err)
}
})
}
}
func TestCreateOrMutateConfigMap(t *testing.T) {
testCreateOrMutate(t, "configmap", "configmaps", &v1.ConfigMap{},
func(client kubernetes.Interface, obj *v1.ConfigMap) kubernetesInterface[*v1.ConfigMap] {
return client.CoreV1().ConfigMaps(obj.ObjectMeta.Namespace)
})
}
func testCreateOrRetain[T kubernetesObject](t *testing.T, resource, resources string, empty T, clientBuilder func(kubernetes.Interface, T) kubernetesInterface[T]) {
tests := []struct {
nameFormat string
setupClient func(*clientsetfake.Clientset)
expectedError bool
}{
{
nameFormat: "%s exists",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("get", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, empty, nil
})
},
expectedError: false,
},
{
nameFormat: "%s get returns an error",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("get", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("")
})
},
expectedError: true,
},
{
nameFormat: "%s is not found, create it",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("get", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewNotFound(schema.GroupResource{}, "name")
})
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
},
expectedError: false,
},
{
nameFormat: "%s is not found, create returns an error",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("get", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewNotFound(schema.GroupResource{}, "name")
})
client.PrependReactor("create", resources, func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("")
})
},
expectedError: true,
},
}
for _, tc := range tests {
t.Run(fmt.Sprintf(tc.nameFormat, resource), func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
tc.setupClient(client)
err := CreateOrRetain[T](clientBuilder(client, empty), empty, resource)
if (err != nil) != tc.expectedError {
t.Fatalf("expected error: %v, got %v, error: %v", tc.expectedError, err != nil, err)
}
})
}
}
func TestCreateOrRetainConfigMap(t *testing.T) {
testCreateOrRetain(t, "configmap", "configmaps", &v1.ConfigMap{},
func(client kubernetes.Interface, obj *v1.ConfigMap) kubernetesInterface[*v1.ConfigMap] {
return client.CoreV1().ConfigMaps(obj.ObjectMeta.Namespace)
})
}
func TestCreateOrUpdateSecret(t *testing.T) {
testCreateOrUpdate(t, "secret", "secrets", &v1.Secret{},
func(client kubernetes.Interface, obj *v1.Secret) kubernetesInterface[*v1.Secret] {
return client.CoreV1().Secrets(obj.ObjectMeta.Namespace)
})
}
func TestCreateOrUpdateServiceAccount(t *testing.T) {
testCreateOrUpdate(t, "serviceaccount", "serviceaccounts", &v1.ServiceAccount{},
func(client kubernetes.Interface, obj *v1.ServiceAccount) kubernetesInterface[*v1.ServiceAccount] {
return client.CoreV1().ServiceAccounts(obj.ObjectMeta.Namespace)
})
}
func TestCreateOrUpdateDeployment(t *testing.T) {
testCreateOrUpdate(t, "deployment", "deployments", &apps.Deployment{},
func(client kubernetes.Interface, obj *apps.Deployment) kubernetesInterface[*apps.Deployment] {
return client.AppsV1().Deployments(obj.ObjectMeta.Namespace)
})
}
func TestCreateOrRetainDeployment(t *testing.T) {
testCreateOrRetain(t, "deployment", "deployments", &apps.Deployment{},
func(client kubernetes.Interface, obj *apps.Deployment) kubernetesInterface[*apps.Deployment] {
return client.AppsV1().Deployments(obj.ObjectMeta.Namespace)
})
}
func TestCreateOrUpdateDaemonSet(t *testing.T) {
testCreateOrUpdate(t, "daemonset", "daemonsets", &apps.DaemonSet{},
func(client kubernetes.Interface, obj *apps.DaemonSet) kubernetesInterface[*apps.DaemonSet] {
return client.AppsV1().DaemonSets(obj.ObjectMeta.Namespace)
})
}
func TestCreateOrUpdateRole(t *testing.T) {
testCreateOrUpdate(t, "role", "roles", &rbac.Role{},
func(client kubernetes.Interface, obj *rbac.Role) kubernetesInterface[*rbac.Role] {
return client.RbacV1().Roles(obj.ObjectMeta.Namespace)
})
}
func TestCreateOrUpdateRoleBindings(t *testing.T) {
testCreateOrUpdate(t, "rolebinding", "rolebindings", &rbac.RoleBinding{},
func(client kubernetes.Interface, obj *rbac.RoleBinding) kubernetesInterface[*rbac.RoleBinding] {
return client.RbacV1().RoleBindings(obj.ObjectMeta.Namespace)
})
}
func TestCreateOrUpdateClusterRole(t *testing.T) {
testCreateOrUpdate(t, "clusterrole", "clusterroles", &rbac.ClusterRole{},
func(client kubernetes.Interface, obj *rbac.ClusterRole) kubernetesInterface[*rbac.ClusterRole] {
return client.RbacV1().ClusterRoles()
})
}
func TestCreateOrUpdateClusterRoleBindings(t *testing.T) {
testCreateOrUpdate(t, "clusterrolebinding", "clusterrolebindings", &rbac.ClusterRoleBinding{},
func(client kubernetes.Interface, obj *rbac.ClusterRoleBinding) kubernetesInterface[*rbac.ClusterRoleBinding] {
return client.RbacV1().ClusterRoleBindings()
})
}
func TestPatchNodeOnce(t *testing.T) {
testcases := []struct {
name string
lookupName string
node v1.Node
success bool
fakeError error
}{
{
name: "simple update",
lookupName: "testnode",
node: v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "testnode",
Labels: map[string]string{v1.LabelHostname: ""},
},
},
success: true,
},
{
name: "node does not exist",
lookupName: "whale",
success: false,
},
{
name: "node not labelled yet",
lookupName: "robin",
node: v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "robin",
},
},
success: false,
},
{
name: "patch node when timeout",
lookupName: "testnode",
node: v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "testnode",
Labels: map[string]string{v1.LabelHostname: ""},
},
},
success: false,
fakeError: apierrors.NewTimeoutError("fake timeout", -1),
},
{
name: "patch node when conflict",
lookupName: "testnode",
node: v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "testnode",
Labels: map[string]string{v1.LabelHostname: ""},
},
},
success: false,
fakeError: apierrors.NewConflict(schema.GroupResource{}, "fake conflict", nil),
},
{
name: "patch node when there is a server timeout",
lookupName: "testnode",
node: v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "testnode",
Labels: map[string]string{v1.LabelHostname: ""},
},
},
success: false,
fakeError: apierrors.NewServerTimeout(schema.GroupResource{}, "fake server timeout", 1),
},
{
name: "patch node when the service is unavailable",
lookupName: "testnode",
node: v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "testnode",
Labels: map[string]string{v1.LabelHostname: ""},
},
},
success: false,
fakeError: apierrors.NewServiceUnavailable("fake service unavailable"),
},
{
name: "patch node failed with unknown error",
lookupName: "testnode",
node: v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "testnode",
Labels: map[string]string{v1.LabelHostname: ""},
},
},
success: false,
fakeError: errors.New("unknown error"),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
_, err := client.CoreV1().Nodes().Create(context.Background(), &tc.node, metav1.CreateOptions{})
if err != nil {
t.Fatalf("failed to create node to fake client: %v", err)
}
if tc.fakeError != nil {
client.PrependReactor("patch", "nodes", func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, tc.fakeError
})
}
var lastError error
conditionFunction := PatchNodeOnce(client, tc.lookupName, func(node *v1.Node) {
node.Annotations = map[string]string{
"updatedBy": "test",
}
}, &lastError)
success, err := conditionFunction(context.Background())
if err != nil && tc.success {
t.Fatalf("did not expect error: %v", err)
}
if success != tc.success {
t.Fatalf("expected %v got %v", tc.success, success)
}
})
}
}
func TestPatchNode(t *testing.T) {
tests := []struct {
name string
setupClient func(*clientsetfake.Clientset)
expectedError bool
}{
{
name: "success",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("get", "nodes", func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, &v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "some-node",
Labels: map[string]string{v1.LabelHostname: ""},
},
}, nil
})
client.PrependReactor("patch", "nodes", func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
},
expectedError: false,
},
{
name: "error",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("get", "nodes", func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("unknown error")
})
},
expectedError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
tc.setupClient(client)
patchFn := func(*v1.Node) {}
err := PatchNode(client, "some-node", patchFn)
if (err != nil) != tc.expectedError {
t.Fatalf("expected error: %v, got %v, error: %v", tc.expectedError, err != nil, err)
}
})
}
}
func TestGetConfigMapWithShortRetry(t *testing.T) {
expectedConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns",
Name: "some-cm",
},
}
tests := []struct {
name string
setupClient func(*clientsetfake.Clientset)
expectedConfigMap *v1.ConfigMap
expectedError bool
}{
{
name: "configmap exists",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("get", "configmaps", func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, expectedConfigMap, nil
})
},
expectedConfigMap: expectedConfigMap,
expectedError: false,
},
{
name: "configmap get returns an error",
setupClient: func(client *clientsetfake.Clientset) {
client.PrependReactor("get", "configmaps", func(clientgotesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("")
})
},
expectedError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
tc.setupClient(client)
actual, err := GetConfigMapWithShortRetry(client, "ns", "some-cm")
if (err != nil) != tc.expectedError {
t.Fatalf("expected error: %v, got %v, error: %v", tc.expectedError, err != nil, err)
}
if err != nil {
return
}
diff := cmp.Diff(tc.expectedConfigMap, actual)
if len(diff) > 0 {
t.Fatalf("got a diff with the expected config (-want,+got):\n%s", diff)
}
})
}
}