constellation/bootstrapper/internal/joinclient/client_test.go

444 lines
11 KiB
Go
Raw Normal View History

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
2022-06-21 11:59:12 -04:00
package joinclient
import (
"context"
"errors"
"net"
"strconv"
"sync"
"testing"
"time"
2022-09-21 07:47:57 -04:00
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/grpc/atlscredentials"
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
"github.com/edgelesssys/constellation/v2/internal/grpc/testdialer"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/role"
"github.com/edgelesssys/constellation/v2/internal/versions"
2022-09-21 07:47:57 -04:00
"github.com/edgelesssys/constellation/v2/joinservice/joinproto"
2022-06-21 11:59:12 -04:00
"github.com/spf13/afero"
2022-06-28 12:33:27 -04:00
"github.com/stretchr/testify/assert"
2022-07-05 07:33:03 -04:00
"github.com/stretchr/testify/require"
2022-06-21 11:59:12 -04:00
"go.uber.org/goleak"
"google.golang.org/grpc"
2022-06-28 12:33:27 -04:00
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
2022-06-21 11:59:12 -04:00
testclock "k8s.io/utils/clock/testing"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestClient(t *testing.T) {
someErr := errors.New("failed")
lockedLock := newFakeLock()
aqcuiredLock, lockErr := lockedLock.TryLockOnce(nil)
require.True(t, aqcuiredLock)
require.Nil(t, lockErr)
2022-07-05 07:33:03 -04:00
workerSelf := metadata.InstanceMetadata{Role: role.Worker, Name: "node-1"}
controlSelf := metadata.InstanceMetadata{Role: role.ControlPlane, Name: "node-5"}
2022-06-21 11:59:12 -04:00
peers := []metadata.InstanceMetadata{
{Role: role.Worker, Name: "node-2", VPCIP: "192.0.2.8"},
{Role: role.ControlPlane, Name: "node-3", VPCIP: "192.0.2.1"},
{Role: role.ControlPlane, Name: "node-4", VPCIP: "192.0.2.2"},
{Role: role.ControlPlane, Name: "node-5", VPCIP: "192.0.2.3"},
2022-06-21 11:59:12 -04:00
}
testCases := map[string]struct {
role role.Role
2022-06-28 12:33:27 -04:00
clusterJoiner *stubClusterJoiner
disk encryptedDisk
nodeLock *fakeLock
2022-06-21 11:59:12 -04:00
apiAnswers []any
2022-07-05 08:14:11 -04:00
wantLock bool
wantJoin bool
2022-06-21 11:59:12 -04:00
}{
"on worker: metadata self: errors occur": {
role: role.Worker,
2022-06-21 11:59:12 -04:00
apiAnswers: []any{
selfAnswer{err: someErr},
selfAnswer{err: someErr},
selfAnswer{err: someErr},
2022-07-05 07:33:03 -04:00
selfAnswer{instance: workerSelf},
2022-06-21 11:59:12 -04:00
listAnswer{instances: peers},
2022-07-05 05:41:31 -04:00
issueJoinTicketAnswer{},
2022-06-21 11:59:12 -04:00
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
2022-06-21 11:59:12 -04:00
disk: &stubDisk{},
2022-07-05 08:14:11 -04:00
wantJoin: true,
wantLock: true,
2022-06-21 11:59:12 -04:00
},
"on worker: metadata self: invalid answer": {
role: role.Worker,
2022-06-21 11:59:12 -04:00
apiAnswers: []any{
selfAnswer{},
selfAnswer{instance: metadata.InstanceMetadata{Role: role.Worker}},
2022-06-21 11:59:12 -04:00
selfAnswer{instance: metadata.InstanceMetadata{Name: "node-1"}},
2022-07-05 07:33:03 -04:00
selfAnswer{instance: workerSelf},
2022-06-21 11:59:12 -04:00
listAnswer{instances: peers},
2022-07-05 05:41:31 -04:00
issueJoinTicketAnswer{},
2022-06-21 11:59:12 -04:00
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
2022-06-21 11:59:12 -04:00
disk: &stubDisk{},
2022-07-05 08:14:11 -04:00
wantJoin: true,
wantLock: true,
2022-06-21 11:59:12 -04:00
},
"on worker: metadata list: errors occur": {
role: role.Worker,
2022-06-21 11:59:12 -04:00
apiAnswers: []any{
2022-07-05 07:33:03 -04:00
selfAnswer{instance: workerSelf},
2022-06-21 11:59:12 -04:00
listAnswer{err: someErr},
listAnswer{err: someErr},
listAnswer{err: someErr},
listAnswer{instances: peers},
2022-07-05 05:41:31 -04:00
issueJoinTicketAnswer{},
2022-06-21 11:59:12 -04:00
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
2022-06-21 11:59:12 -04:00
disk: &stubDisk{},
2022-07-05 08:14:11 -04:00
wantJoin: true,
wantLock: true,
2022-06-21 11:59:12 -04:00
},
"on worker: metadata list: no control plane nodes in answer": {
role: role.Worker,
2022-06-21 11:59:12 -04:00
apiAnswers: []any{
2022-07-05 07:33:03 -04:00
selfAnswer{instance: workerSelf},
2022-06-21 11:59:12 -04:00
listAnswer{},
listAnswer{},
listAnswer{},
listAnswer{instances: peers},
2022-07-05 05:41:31 -04:00
issueJoinTicketAnswer{},
2022-06-21 11:59:12 -04:00
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
2022-06-21 11:59:12 -04:00
disk: &stubDisk{},
2022-07-05 08:14:11 -04:00
wantJoin: true,
wantLock: true,
2022-06-21 11:59:12 -04:00
},
2022-07-05 07:33:03 -04:00
"on worker: issueJoinTicket errors": {
role: role.Worker,
2022-06-21 11:59:12 -04:00
apiAnswers: []any{
2022-07-05 07:33:03 -04:00
selfAnswer{instance: workerSelf},
2022-06-21 11:59:12 -04:00
listAnswer{instances: peers},
2022-07-05 05:41:31 -04:00
issueJoinTicketAnswer{err: someErr},
2022-06-21 11:59:12 -04:00
listAnswer{instances: peers},
2022-07-05 05:41:31 -04:00
issueJoinTicketAnswer{err: someErr},
2022-06-21 11:59:12 -04:00
listAnswer{instances: peers},
2022-07-05 05:41:31 -04:00
issueJoinTicketAnswer{},
2022-06-21 11:59:12 -04:00
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
2022-06-21 11:59:12 -04:00
disk: &stubDisk{},
2022-07-05 08:14:11 -04:00
wantJoin: true,
wantLock: true,
2022-06-21 11:59:12 -04:00
},
2022-07-05 07:33:03 -04:00
"on control plane: issueJoinTicket errors": {
role: role.ControlPlane,
apiAnswers: []any{
selfAnswer{instance: controlSelf},
listAnswer{instances: peers},
issueJoinTicketAnswer{err: someErr},
listAnswer{instances: peers},
issueJoinTicketAnswer{err: someErr},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
2022-07-05 07:33:03 -04:00
disk: &stubDisk{},
2022-07-05 08:14:11 -04:00
wantJoin: true,
wantLock: true,
2022-07-05 07:33:03 -04:00
},
"on control plane: joinCluster fails": {
role: role.ControlPlane,
apiAnswers: []any{
selfAnswer{instance: controlSelf},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
},
clusterJoiner: &stubClusterJoiner{joinClusterErr: someErr},
nodeLock: newFakeLock(),
2022-07-05 07:33:03 -04:00
disk: &stubDisk{},
2022-07-05 08:14:11 -04:00
wantJoin: true,
wantLock: true,
2022-07-05 07:33:03 -04:00
},
"on control plane: node already locked": {
role: role.ControlPlane,
apiAnswers: []any{
selfAnswer{instance: controlSelf},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: lockedLock,
disk: &stubDisk{},
2022-07-05 08:14:11 -04:00
wantLock: true,
2022-07-05 07:33:03 -04:00
},
"on control plane: disk open fails": {
role: role.ControlPlane,
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
2022-07-05 07:33:03 -04:00
disk: &stubDisk{openErr: someErr},
},
"on control plane: disk uuid fails": {
role: role.ControlPlane,
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
2022-07-05 07:33:03 -04:00
disk: &stubDisk{uuidErr: someErr},
},
2022-06-21 11:59:12 -04:00
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
2022-06-28 12:33:27 -04:00
assert := assert.New(t)
2022-06-21 11:59:12 -04:00
clock := testclock.NewFakeClock(time.Now())
metadataAPI := newStubMetadataAPI()
fileHandler := file.NewHandler(afero.NewMemMapFs())
netDialer := testdialer.NewBufconnDialer()
dialer := dialer.New(nil, nil, netDialer)
client := &JoinClient{
nodeLock: tc.nodeLock,
timeout: 30 * time.Second,
interval: time.Millisecond,
dialer: dialer,
disk: tc.disk,
joiner: tc.clusterJoiner,
fileHandler: fileHandler,
metadataAPI: metadataAPI,
clock: clock,
log: logger.NewTest(t),
2022-06-21 11:59:12 -04:00
}
serverCreds := atlscredentials.New(nil, nil)
2022-07-05 05:41:31 -04:00
joinServer := grpc.NewServer(grpc.Creds(serverCreds))
joinserviceAPI := newStubJoinServiceAPI()
joinproto.RegisterAPIServer(joinServer, joinserviceAPI)
port := strconv.Itoa(constants.JoinServiceNodePort)
2022-06-21 11:59:12 -04:00
listener := netDialer.GetListener(net.JoinHostPort("192.0.2.3", port))
2022-07-05 05:41:31 -04:00
go joinServer.Serve(listener)
defer joinServer.GracefulStop()
2022-06-21 11:59:12 -04:00
client.Start(stubCleaner{})
2022-06-21 11:59:12 -04:00
for _, a := range tc.apiAnswers {
switch a := a.(type) {
case selfAnswer:
metadataAPI.selfAnswerC <- a
case listAnswer:
metadataAPI.listAnswerC <- a
2022-07-05 05:41:31 -04:00
case issueJoinTicketAnswer:
joinserviceAPI.issueJoinTicketAnswerC <- a
2022-06-21 11:59:12 -04:00
}
clock.Step(time.Second)
}
client.Stop()
2022-06-28 12:33:27 -04:00
2022-07-05 08:14:11 -04:00
if tc.wantJoin {
assert.True(tc.clusterJoiner.joinClusterCalled)
} else {
assert.False(tc.clusterJoiner.joinClusterCalled)
}
if tc.wantLock {
assert.False(client.nodeLock.TryLockOnce(nil)) // lock should be locked
2022-07-05 08:14:11 -04:00
} else {
assert.True(client.nodeLock.TryLockOnce(nil))
2022-07-05 08:14:11 -04:00
}
2022-06-21 11:59:12 -04:00
})
}
}
func TestClientConcurrentStartStop(t *testing.T) {
2022-06-28 12:33:27 -04:00
netDialer := testdialer.NewBufconnDialer()
dialer := dialer.New(nil, nil, netDialer)
2022-06-21 11:59:12 -04:00
client := &JoinClient{
nodeLock: newFakeLock(),
2022-06-28 12:33:27 -04:00
timeout: 30 * time.Second,
interval: 30 * time.Second,
dialer: dialer,
disk: &stubDisk{},
joiner: &stubClusterJoiner{},
fileHandler: file.NewHandler(afero.NewMemMapFs()),
2022-06-21 11:59:12 -04:00
metadataAPI: &stubRepeaterMetadataAPI{},
clock: testclock.NewFakeClock(time.Now()),
log: logger.NewTest(t),
2022-06-21 11:59:12 -04:00
}
wg := sync.WaitGroup{}
start := func() {
defer wg.Done()
client.Start(stubCleaner{})
2022-06-21 11:59:12 -04:00
}
stop := func() {
defer wg.Done()
client.Stop()
}
wg.Add(10)
go stop()
go start()
go start()
go stop()
go stop()
go start()
go start()
go stop()
go stop()
go start()
wg.Wait()
client.Stop()
}
2022-07-05 07:33:03 -04:00
func TestIsUnrecoverable(t *testing.T) {
assert := assert.New(t)
some := errors.New("failed")
unrec := unrecoverableError{some}
assert.True(isUnrecoverable(unrec))
assert.False(isUnrecoverable(some))
}
2022-06-21 11:59:12 -04:00
type stubRepeaterMetadataAPI struct {
selfInstance metadata.InstanceMetadata
selfErr error
listInstances []metadata.InstanceMetadata
listErr error
}
func (s *stubRepeaterMetadataAPI) Self(_ context.Context) (metadata.InstanceMetadata, error) {
return s.selfInstance, s.selfErr
}
func (s *stubRepeaterMetadataAPI) List(_ context.Context) ([]metadata.InstanceMetadata, error) {
return s.listInstances, s.listErr
}
type stubMetadataAPI struct {
selfAnswerC chan selfAnswer
listAnswerC chan listAnswer
}
func newStubMetadataAPI() *stubMetadataAPI {
return &stubMetadataAPI{
selfAnswerC: make(chan selfAnswer),
listAnswerC: make(chan listAnswer),
}
}
func (s *stubMetadataAPI) Self(_ context.Context) (metadata.InstanceMetadata, error) {
answer := <-s.selfAnswerC
return answer.instance, answer.err
}
func (s *stubMetadataAPI) List(_ context.Context) ([]metadata.InstanceMetadata, error) {
answer := <-s.listAnswerC
return answer.instances, answer.err
}
type selfAnswer struct {
instance metadata.InstanceMetadata
err error
}
type listAnswer struct {
instances []metadata.InstanceMetadata
err error
}
2022-07-05 05:41:31 -04:00
type stubJoinServiceAPI struct {
issueJoinTicketAnswerC chan issueJoinTicketAnswer
2022-06-21 11:59:12 -04:00
2022-07-05 05:41:31 -04:00
joinproto.UnimplementedAPIServer
2022-06-21 11:59:12 -04:00
}
2022-07-05 05:41:31 -04:00
func newStubJoinServiceAPI() *stubJoinServiceAPI {
return &stubJoinServiceAPI{
issueJoinTicketAnswerC: make(chan issueJoinTicketAnswer),
2022-06-21 11:59:12 -04:00
}
}
2022-07-05 05:41:31 -04:00
func (s *stubJoinServiceAPI) IssueJoinTicket(_ context.Context, _ *joinproto.IssueJoinTicketRequest,
) (*joinproto.IssueJoinTicketResponse, error) {
answer := <-s.issueJoinTicketAnswerC
2022-06-21 11:59:12 -04:00
if answer.resp == nil {
2022-07-05 08:14:11 -04:00
answer.resp = &joinproto.IssueJoinTicketResponse{}
2022-06-21 11:59:12 -04:00
}
return answer.resp, answer.err
}
2022-07-05 05:41:31 -04:00
type issueJoinTicketAnswer struct {
resp *joinproto.IssueJoinTicketResponse
2022-06-21 11:59:12 -04:00
err error
}
type stubClusterJoiner struct {
2022-06-28 12:33:27 -04:00
joinClusterCalled bool
joinClusterErr error
2022-06-21 11:59:12 -04:00
}
func (j *stubClusterJoiner) JoinCluster(context.Context, *kubeadm.BootstrapTokenDiscovery, role.Role, string, versions.ComponentVersions, *logger.Logger) error {
2022-06-28 12:33:27 -04:00
j.joinClusterCalled = true
2022-06-21 11:59:12 -04:00
return j.joinClusterErr
}
type stubDisk struct {
openErr error
closeErr error
uuid string
uuidErr error
updatePassphraseErr error
updatePassphraseCalled bool
}
func (d *stubDisk) Open() error {
return d.openErr
}
func (d *stubDisk) Close() error {
return d.closeErr
}
func (d *stubDisk) UUID() (string, error) {
return d.uuid, d.uuidErr
}
func (d *stubDisk) UpdatePassphrase(string) error {
d.updatePassphraseCalled = true
return d.updatePassphraseErr
}
type stubCleaner struct{}
func (c stubCleaner) Clean() {}
type fakeLock struct {
state *sync.Mutex
}
func newFakeLock() *fakeLock {
return &fakeLock{
state: &sync.Mutex{},
}
}
func (l *fakeLock) TryLockOnce(_ []byte) (bool, error) {
return l.state.TryLock(), nil
}