kubernetes/cmd/kubeadm/app/util/etcd/etcd_test.go
Lubomir I. Ivanov 5f876b9d0a kubeadm: switch from ExponentialBackoff() to PollUntilContextTimeout()
Switch to PollUntilContextTimeout() everywhere to allow
usage of the exposed timeouts in the kubeadm API. Exponential backoff
options are more difficult to expose in this regard and a bit too
detailed for the common user - i.e. have "steps", "factor" and so on.
2024-01-14 15:07:56 +02:00

791 lines
20 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 etcd
import (
"context"
"fmt"
"reflect"
"strconv"
"testing"
"time"
"github.com/pkg/errors"
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
clientv3 "go.etcd.io/etcd/client/v3"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
clientsetfake "k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
testresources "k8s.io/kubernetes/cmd/kubeadm/test/resources"
)
var errNotImplemented = errors.New("not implemented")
type fakeEtcdClient struct {
members []*pb.Member
endpoints []string
}
// Close shuts down the client's etcd connections.
func (f *fakeEtcdClient) Close() error {
f.members = []*pb.Member{}
return nil
}
// Endpoints lists the registered endpoints for the client.
func (f *fakeEtcdClient) Endpoints() []string {
return f.endpoints
}
// MemberList lists the current cluster membership.
func (f *fakeEtcdClient) MemberList(_ context.Context) (*clientv3.MemberListResponse, error) {
return &clientv3.MemberListResponse{
Members: f.members,
}, nil
}
// MemberAdd adds a new member into the cluster.
func (f *fakeEtcdClient) MemberAdd(_ context.Context, peerAddrs []string) (*clientv3.MemberAddResponse, error) {
return nil, errNotImplemented
}
// MemberAddAsLearner adds a new learner member into the cluster.
func (f *fakeEtcdClient) MemberAddAsLearner(_ context.Context, peerAddrs []string) (*clientv3.MemberAddResponse, error) {
return nil, errNotImplemented
}
// MemberRemove removes an existing member from the cluster.
func (f *fakeEtcdClient) MemberRemove(_ context.Context, id uint64) (*clientv3.MemberRemoveResponse, error) {
return nil, errNotImplemented
}
// MemberPromote promotes a member from raft learner (non-voting) to raft voting member.
func (f *fakeEtcdClient) MemberPromote(_ context.Context, id uint64) (*clientv3.MemberPromoteResponse, error) {
return nil, errNotImplemented
}
// Status gets the status of the endpoint.
func (f *fakeEtcdClient) Status(_ context.Context, endpoint string) (*clientv3.StatusResponse, error) {
return nil, errNotImplemented
}
// Sync synchronizes client's endpoints with the known endpoints from the etcd membership.
func (f *fakeEtcdClient) Sync(_ context.Context) error {
return errNotImplemented
}
func testGetURL(t *testing.T, getURLFunc func(*kubeadmapi.APIEndpoint) string, port int) {
portStr := strconv.Itoa(port)
tests := []struct {
name string
advertiseAddress string
expectedURL string
}{
{
name: "IPv4",
advertiseAddress: "10.10.10.10",
expectedURL: fmt.Sprintf("https://10.10.10.10:%s", portStr),
},
{
name: "IPv6",
advertiseAddress: "2001:db8::2",
expectedURL: fmt.Sprintf("https://[2001:db8::2]:%s", portStr),
},
{
name: "IPv4 localhost",
advertiseAddress: "127.0.0.1",
expectedURL: fmt.Sprintf("https://127.0.0.1:%s", portStr),
},
{
name: "IPv6 localhost",
advertiseAddress: "::1",
expectedURL: fmt.Sprintf("https://[::1]:%s", portStr),
},
}
for _, test := range tests {
url := getURLFunc(&kubeadmapi.APIEndpoint{AdvertiseAddress: test.advertiseAddress})
if url != test.expectedURL {
t.Errorf("expected %s, got %s", test.expectedURL, url)
}
}
}
func TestGetClientURL(t *testing.T) {
testGetURL(t, GetClientURL, constants.EtcdListenClientPort)
}
func TestGetPeerURL(t *testing.T) {
testGetURL(t, GetPeerURL, constants.EtcdListenPeerPort)
}
func TestGetClientURLByIP(t *testing.T) {
portStr := strconv.Itoa(constants.EtcdListenClientPort)
tests := []struct {
name string
ip string
expectedURL string
}{
{
name: "IPv4",
ip: "10.10.10.10",
expectedURL: fmt.Sprintf("https://10.10.10.10:%s", portStr),
},
{
name: "IPv6",
ip: "2001:db8::2",
expectedURL: fmt.Sprintf("https://[2001:db8::2]:%s", portStr),
},
{
name: "IPv4 localhost",
ip: "127.0.0.1",
expectedURL: fmt.Sprintf("https://127.0.0.1:%s", portStr),
},
{
name: "IPv6 localhost",
ip: "::1",
expectedURL: fmt.Sprintf("https://[::1]:%s", portStr),
},
}
for _, test := range tests {
url := GetClientURLByIP(test.ip)
if url != test.expectedURL {
t.Errorf("expected %s, got %s", test.expectedURL, url)
}
}
}
func TestGetEtcdEndpointsWithBackoff(t *testing.T) {
tests := []struct {
name string
pods []testresources.FakeStaticPod
expectedEndpoints []string
expectedErr bool
}{
{
name: "no pod annotations",
expectedEndpoints: []string{},
expectedErr: true,
},
{
name: "ipv4 endpoint in pod annotation; port is preserved",
pods: []testresources.FakeStaticPod{
{
Component: constants.Etcd,
Annotations: map[string]string{
constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.4:1234",
},
},
},
expectedEndpoints: []string{"https://1.2.3.4:1234"},
},
}
for _, rt := range tests {
t.Run(rt.name, func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
for _, pod := range rt.pods {
if err := pod.Create(client); err != nil {
t.Errorf("error setting up test creating pod for node %q", pod.NodeName)
}
}
endpoints, err := getEtcdEndpointsWithRetry(client, time.Microsecond*10, time.Millisecond*100)
if err != nil && !rt.expectedErr {
t.Errorf("got error %q; was expecting no errors", err)
return
} else if err == nil && rt.expectedErr {
t.Error("got no error; was expecting an error")
return
} else if err != nil && rt.expectedErr {
return
}
if !reflect.DeepEqual(endpoints, rt.expectedEndpoints) {
t.Errorf("expected etcd endpoints: %v; got: %v", rt.expectedEndpoints, endpoints)
}
})
}
}
func TestGetRawEtcdEndpointsFromPodAnnotation(t *testing.T) {
tests := []struct {
name string
pods []testresources.FakeStaticPod
clientSetup func(*clientsetfake.Clientset)
expectedEndpoints []string
expectedErr bool
}{
{
name: "exactly one pod with annotation",
pods: []testresources.FakeStaticPod{
{
NodeName: "cp-0",
Component: constants.Etcd,
Annotations: map[string]string{constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.4:2379"},
},
},
expectedEndpoints: []string{"https://1.2.3.4:2379"},
},
{
name: "two pods; one is missing annotation",
pods: []testresources.FakeStaticPod{
{
NodeName: "cp-0",
Component: constants.Etcd,
Annotations: map[string]string{constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.4:2379"},
},
{
NodeName: "cp-1",
Component: constants.Etcd,
},
},
expectedEndpoints: []string{"https://1.2.3.4:2379"},
expectedErr: true,
},
{
name: "no pods with annotation",
expectedErr: true,
},
{
name: "exactly one pod with annotation; all requests fail",
pods: []testresources.FakeStaticPod{
{
NodeName: "cp-0",
Component: constants.Etcd,
Annotations: map[string]string{constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.4:2379"},
},
},
clientSetup: func(clientset *clientsetfake.Clientset) {
clientset.PrependReactor("list", "pods", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, apierrors.NewInternalError(errors.New("API server down"))
})
},
expectedErr: true,
},
}
for _, rt := range tests {
t.Run(rt.name, func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
for i, pod := range rt.pods {
if err := pod.CreateWithPodSuffix(client, strconv.Itoa(i)); err != nil {
t.Errorf("error setting up test creating pod for node %q", pod.NodeName)
}
}
if rt.clientSetup != nil {
rt.clientSetup(client)
}
endpoints, err := getRawEtcdEndpointsFromPodAnnotation(client, time.Microsecond*10, time.Millisecond*100)
if err != nil && !rt.expectedErr {
t.Errorf("got error %v, but wasn't expecting any error", err)
return
} else if err == nil && rt.expectedErr {
t.Error("didn't get any error; but was expecting an error")
return
} else if err != nil && rt.expectedErr {
return
}
if !reflect.DeepEqual(endpoints, rt.expectedEndpoints) {
t.Errorf("expected etcd endpoints: %v; got: %v", rt.expectedEndpoints, endpoints)
}
})
}
}
func TestGetRawEtcdEndpointsFromPodAnnotationWithoutRetry(t *testing.T) {
tests := []struct {
name string
pods []testresources.FakeStaticPod
clientSetup func(*clientsetfake.Clientset)
expectedEndpoints []string
expectedErr bool
}{
{
name: "no pods",
expectedEndpoints: []string{},
},
{
name: "exactly one pod with annotation",
pods: []testresources.FakeStaticPod{
{
NodeName: "cp-0",
Component: constants.Etcd,
Annotations: map[string]string{constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.4:2379"},
},
},
expectedEndpoints: []string{"https://1.2.3.4:2379"},
},
{
name: "two pods; one is missing annotation",
pods: []testresources.FakeStaticPod{
{
NodeName: "cp-0",
Component: constants.Etcd,
Annotations: map[string]string{constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.4:2379"},
},
{
NodeName: "cp-1",
Component: constants.Etcd,
},
},
expectedEndpoints: []string{"https://1.2.3.4:2379"},
},
{
name: "two pods with annotation",
pods: []testresources.FakeStaticPod{
{
NodeName: "cp-0",
Component: constants.Etcd,
Annotations: map[string]string{constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.4:2379"},
},
{
NodeName: "cp-1",
Component: constants.Etcd,
Annotations: map[string]string{constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.5:2379"},
},
},
expectedEndpoints: []string{"https://1.2.3.4:2379", "https://1.2.3.5:2379"},
},
{
name: "exactly one pod with annotation; request fails",
pods: []testresources.FakeStaticPod{
{
NodeName: "cp-0",
Component: constants.Etcd,
Annotations: map[string]string{constants.EtcdAdvertiseClientUrlsAnnotationKey: "https://1.2.3.4:2379"},
},
},
clientSetup: func(clientset *clientsetfake.Clientset) {
clientset.PrependReactor("list", "pods", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, apierrors.NewInternalError(errors.New("API server down"))
})
},
expectedErr: true,
},
}
for _, rt := range tests {
t.Run(rt.name, func(t *testing.T) {
client := clientsetfake.NewSimpleClientset()
for _, pod := range rt.pods {
if err := pod.Create(client); err != nil {
t.Errorf("error setting up test creating pod for node %q", pod.NodeName)
return
}
}
if rt.clientSetup != nil {
rt.clientSetup(client)
}
endpoints, _, err := getRawEtcdEndpointsFromPodAnnotationWithoutRetry(client)
if err != nil && !rt.expectedErr {
t.Errorf("got error %v, but wasn't expecting any error", err)
return
} else if err == nil && rt.expectedErr {
t.Error("didn't get any error; but was expecting an error")
return
} else if err != nil && rt.expectedErr {
return
}
if !reflect.DeepEqual(endpoints, rt.expectedEndpoints) {
t.Errorf("expected etcd endpoints: %v; got: %v", rt.expectedEndpoints, endpoints)
}
})
}
}
func TestClient_GetMemberID(t *testing.T) {
type fields struct {
Endpoints []string
newEtcdClient func(endpoints []string) (etcdClient, error)
}
type args struct {
peerURL string
}
tests := []struct {
name string
fields fields
args args
want uint64
wantErr error
}{
{
name: "member ID found",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
ID: 1,
Name: "member1",
PeerURLs: []string{
"https://member1:2380",
},
},
},
}
return f, nil
},
},
args: args{
peerURL: "https://member1:2380",
},
wantErr: nil,
want: 1,
},
{
name: "member ID not found",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
ID: 1,
Name: "member1",
PeerURLs: []string{
"https://member1:2380",
},
},
},
}
return f, nil
},
},
args: args{
peerURL: "https://member2:2380",
},
wantErr: ErrNoMemberIDForPeerURL,
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{
Endpoints: tt.fields.Endpoints,
newEtcdClient: tt.fields.newEtcdClient,
}
c.listMembersFunc = func(_ time.Duration) (*clientv3.MemberListResponse, error) {
f, _ := c.newEtcdClient([]string{})
resp, _ := f.MemberList(context.Background())
return resp, nil
}
got, err := c.GetMemberID(tt.args.peerURL)
if !errors.Is(tt.wantErr, err) {
t.Errorf("Client.GetMemberID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Client.GetMemberID() = %v, want %v", got, tt.want)
}
})
}
}
func TestListMembers(t *testing.T) {
type fields struct {
Endpoints []string
newEtcdClient func(endpoints []string) (etcdClient, error)
listMembersFunc func(timeout time.Duration) (*clientv3.MemberListResponse, error)
}
tests := []struct {
name string
fields fields
want []Member
wantError bool
}{
{
name: "PeerURLs are empty",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{}
return f, nil
},
},
want: []Member{},
},
{
name: "PeerURLs are non-empty",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
ID: 1,
Name: "member1",
PeerURLs: []string{
"https://member1:2380",
},
},
{
ID: 2,
Name: "member2",
PeerURLs: []string{
"https://member2:2380",
},
},
},
}
return f, nil
},
},
want: []Member{
{
Name: "member1",
PeerURL: "https://member1:2380",
},
{
Name: "member2",
PeerURL: "https://member2:2380",
},
},
},
{
name: "PeerURLs has multiple urls",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
ID: 1,
Name: "member1",
PeerURLs: []string{
"https://member1:2380",
"https://member2:2380",
},
},
},
}
return f, nil
},
},
want: []Member{
{
Name: "member1",
PeerURL: "https://member1:2380",
},
},
},
{
name: "ListMembers return error",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
ID: 1,
Name: "member1",
PeerURLs: []string{
"https://member1:2380",
"https://member2:2380",
},
},
},
}
return f, nil
},
listMembersFunc: func(_ time.Duration) (*clientv3.MemberListResponse, error) {
return nil, errNotImplemented
},
},
want: nil,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{
Endpoints: tt.fields.Endpoints,
newEtcdClient: tt.fields.newEtcdClient,
listMembersFunc: tt.fields.listMembersFunc,
}
if c.listMembersFunc == nil {
c.listMembersFunc = func(_ time.Duration) (*clientv3.MemberListResponse, error) {
return c.listMembers(100 * time.Millisecond)
}
}
got, err := c.ListMembers()
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ListMembers() = %v, want %v", got, tt.want)
}
if (err != nil) != (tt.wantError) {
t.Errorf("ListMembers() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestIsLearner(t *testing.T) {
type fields struct {
Endpoints []string
newEtcdClient func(endpoints []string) (etcdClient, error)
listMembersFunc func(timeout time.Duration) (*clientv3.MemberListResponse, error)
}
tests := []struct {
name string
fields fields
memberID uint64
want bool
wantError bool
}{
{
name: "The specified member is not a learner",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
ID: 1,
Name: "member1",
PeerURLs: []string{
"https://member1:2380",
},
IsLearner: false,
},
},
}
return f, nil
},
},
memberID: 1,
want: false,
},
{
name: "The specified member is a learner",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
ID: 1,
Name: "member1",
PeerURLs: []string{
"https://member1:2380",
},
IsLearner: true,
},
{
ID: 2,
Name: "member2",
PeerURLs: []string{
"https://member2:2380",
},
},
},
}
return f, nil
},
},
memberID: 1,
want: true,
},
{
name: "The specified member does not exist",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{},
}
return f, nil
},
},
memberID: 3,
want: false,
},
{
name: "Learner ID is empty",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
Name: "member2",
PeerURLs: []string{
"https://member2:2380",
},
IsLearner: true,
},
},
}
return f, nil
},
},
want: true,
},
{
name: "ListMembers returns an error",
fields: fields{
Endpoints: []string{},
newEtcdClient: func(endpoints []string) (etcdClient, error) {
f := &fakeEtcdClient{
members: []*pb.Member{
{
Name: "member2",
PeerURLs: []string{
"https://member2:2380",
},
IsLearner: true,
},
},
}
return f, nil
},
listMembersFunc: func(_ time.Duration) (*clientv3.MemberListResponse, error) {
return nil, errNotImplemented
},
},
want: false,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{
Endpoints: tt.fields.Endpoints,
newEtcdClient: tt.fields.newEtcdClient,
listMembersFunc: tt.fields.listMembersFunc,
}
if c.listMembersFunc == nil {
c.listMembersFunc = func(t_ time.Duration) (*clientv3.MemberListResponse, error) {
f, _ := c.newEtcdClient([]string{})
resp, _ := f.MemberList(context.Background())
return resp, nil
}
}
got, err := c.isLearner(tt.memberID)
if got != tt.want {
t.Errorf("isLearner() = %v, want %v", got, tt.want)
}
if (err != nil) != (tt.wantError) {
t.Errorf("isLearner() error = %v, wantError %v", err, tt.wantError)
}
})
}
}