constellation/coordinator/cloudprovider/gcp/client_test.go
Leonard Cohnen 2d8fcd9bf4 monorepo
Co-authored-by: Malte Poll <mp@edgeless.systems>
Co-authored-by: katexochen <katexochen@users.noreply.github.com>
Co-authored-by: Daniel Weiße <dw@edgeless.systems>
Co-authored-by: Thomas Tendyck <tt@edgeless.systems>
Co-authored-by: Benedict Schlueter <bs@edgeless.systems>
Co-authored-by: leongross <leon.gross@rub.de>
Co-authored-by: Moritz Eckert <m1gh7ym0@gmail.com>
2022-03-22 16:09:39 +01:00

756 lines
21 KiB
Go

package gcp
import (
"context"
"errors"
"testing"
compute "cloud.google.com/go/compute/apiv1"
"github.com/edgelesssys/constellation/coordinator/core"
"github.com/edgelesssys/constellation/coordinator/role"
gax "github.com/googleapis/gax-go/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"google.golang.org/api/iterator"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m,
// https://github.com/census-instrumentation/opencensus-go/issues/1262
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
// https://github.com/kubernetes/klog/issues/282, https://github.com/kubernetes/klog/issues/188
goleak.IgnoreTopFunction("k8s.io/klog/v2.(*loggingT).flushDaemon"),
)
}
func TestRetrieveInstances(t *testing.T) {
uid := "1234"
someErr := errors.New("failed")
newTestIter := func() *stubInstanceIterator {
return &stubInstanceIterator{
instances: []*computepb.Instance{
{
Name: proto.String("someInstance"),
Metadata: &computepb.Metadata{
Items: []*computepb.Items{
{
Key: proto.String("ssh-keys"),
Value: proto.String("bob:ssh-rsa bobskey"),
},
{
Key: proto.String("key-2"),
Value: proto.String("value-2"),
},
{
Key: proto.String(core.ConstellationUIDMetadataKey),
Value: proto.String(uid),
},
{
Key: proto.String(core.RoleMetadataKey),
Value: proto.String(role.Coordinator.String()),
},
},
},
NetworkInterfaces: []*computepb.NetworkInterface{
{NetworkIP: proto.String("192.0.2.0")},
},
},
},
}
}
testCases := map[string]struct {
client stubInstancesClient
metadata stubMetadataClient
instanceIter *stubInstanceIterator
instanceIterMutator func(*stubInstanceIterator)
expectedInstances []core.Instance
expectErr bool
}{
"retrieve works": {
client: stubInstancesClient{},
metadata: stubMetadataClient{InstanceValue: uid},
instanceIter: newTestIter(),
expectedInstances: []core.Instance{
{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
Role: role.Coordinator,
IPs: []string{"192.0.2.0"},
SSHKeys: map[string][]string{"bob": {"ssh-rsa bobskey"}},
},
},
},
"instance name is null": {
client: stubInstancesClient{},
metadata: stubMetadataClient{InstanceValue: uid},
instanceIter: newTestIter(),
instanceIterMutator: func(sii *stubInstanceIterator) { sii.instances[0].Name = nil },
expectErr: true,
},
"no instance with network ip": {
client: stubInstancesClient{},
metadata: stubMetadataClient{InstanceValue: uid},
instanceIter: newTestIter(),
instanceIterMutator: func(sii *stubInstanceIterator) { sii.instances[0].NetworkInterfaces = nil },
expectedInstances: []core.Instance{
{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
Role: role.Coordinator,
IPs: []string{},
SSHKeys: map[string][]string{"bob": {"ssh-rsa bobskey"}},
},
},
},
"network ip is nil": {
client: stubInstancesClient{},
metadata: stubMetadataClient{InstanceValue: uid},
instanceIter: newTestIter(),
instanceIterMutator: func(sii *stubInstanceIterator) { sii.instances[0].NetworkInterfaces[0].NetworkIP = nil },
expectedInstances: []core.Instance{
{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
Role: role.Coordinator,
IPs: []string{},
SSHKeys: map[string][]string{"bob": {"ssh-rsa bobskey"}},
},
},
},
"constellation id is not set": {
client: stubInstancesClient{},
metadata: stubMetadataClient{InstanceValue: uid},
instanceIter: newTestIter(),
instanceIterMutator: func(sii *stubInstanceIterator) { sii.instances[0].Metadata.Items[2].Key = proto.String("") },
expectedInstances: []core.Instance{},
},
"constellation retrieval fails": {
client: stubInstancesClient{},
metadata: stubMetadataClient{InstanceErr: someErr},
instanceIter: newTestIter(),
expectErr: true,
},
"role is not set": {
client: stubInstancesClient{},
metadata: stubMetadataClient{InstanceValue: uid},
instanceIter: newTestIter(),
instanceIterMutator: func(sii *stubInstanceIterator) { sii.instances[0].Metadata.Items[3].Key = proto.String("") },
expectedInstances: []core.Instance{
{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
Role: role.Unknown,
IPs: []string{"192.0.2.0"},
SSHKeys: map[string][]string{"bob": {"ssh-rsa bobskey"}},
},
},
},
"instance iterator Next() errors": {
client: stubInstancesClient{},
metadata: stubMetadataClient{InstanceValue: uid},
instanceIter: &stubInstanceIterator{nextErr: someErr},
expectErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
if tc.instanceIterMutator != nil {
tc.instanceIterMutator(tc.instanceIter)
}
tc.client.ListInstanceIterator = tc.instanceIter
client := Client{
instanceAPI: tc.client,
metadataAPI: tc.metadata,
}
instances, err := client.RetrieveInstances(context.Background(), "someProject", "someZone")
if tc.expectErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.expectedInstances, instances)
})
}
}
func TestRetrieveInstance(t *testing.T) {
newTestInstance := func() *computepb.Instance {
return &computepb.Instance{
Name: proto.String("someInstance"),
Metadata: &computepb.Metadata{
Items: []*computepb.Items{
{
Key: proto.String("key-1"),
Value: proto.String("value-1"),
},
{
Key: proto.String("key-2"),
Value: proto.String("value-2"),
},
},
},
NetworkInterfaces: []*computepb.NetworkInterface{
{NetworkIP: proto.String("192.0.2.0")},
},
}
}
testCases := map[string]struct {
client stubInstancesClient
clientInstance *computepb.Instance
clientInstanceMutator func(*computepb.Instance)
expectedInstance core.Instance
expectErr bool
}{
"retrieve works": {
client: stubInstancesClient{},
clientInstance: newTestInstance(),
expectedInstance: core.Instance{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
IPs: []string{"192.0.2.0"},
SSHKeys: map[string][]string{},
},
},
"retrieve with SSH key works": {
client: stubInstancesClient{},
clientInstance: newTestInstance(),
clientInstanceMutator: func(i *computepb.Instance) {
i.Metadata.Items[0].Key = proto.String("ssh-keys")
i.Metadata.Items[0].Value = proto.String("bob:ssh-rsa bobskey")
},
expectedInstance: core.Instance{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
IPs: []string{"192.0.2.0"},
SSHKeys: map[string][]string{"bob": {"ssh-rsa bobskey"}},
},
},
"retrieve with Role works": {
client: stubInstancesClient{},
clientInstance: newTestInstance(),
clientInstanceMutator: func(i *computepb.Instance) {
i.Metadata.Items[0].Key = proto.String(core.RoleMetadataKey)
i.Metadata.Items[0].Value = proto.String(role.Coordinator.String())
},
expectedInstance: core.Instance{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
Role: role.Coordinator,
IPs: []string{"192.0.2.0"},
SSHKeys: map[string][]string{},
},
},
"retrieve fails": {
client: stubInstancesClient{
GetErr: errors.New("retrieve error"),
},
clientInstance: nil,
expectErr: true,
},
"metadata item is null": {
client: stubInstancesClient{},
clientInstance: newTestInstance(),
clientInstanceMutator: func(i *computepb.Instance) { i.Metadata.Items[0] = nil },
expectedInstance: core.Instance{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
IPs: []string{"192.0.2.0"},
SSHKeys: map[string][]string{},
},
},
"metadata key is null": {
client: stubInstancesClient{},
clientInstance: newTestInstance(),
clientInstanceMutator: func(i *computepb.Instance) { i.Metadata.Items[0].Key = nil },
expectedInstance: core.Instance{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
IPs: []string{"192.0.2.0"},
SSHKeys: map[string][]string{},
},
},
"metadata value is null": {
client: stubInstancesClient{},
clientInstance: newTestInstance(),
clientInstanceMutator: func(i *computepb.Instance) { i.Metadata.Items[0].Value = nil },
expectedInstance: core.Instance{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
IPs: []string{"192.0.2.0"},
SSHKeys: map[string][]string{},
},
},
"instance without network ip": {
client: stubInstancesClient{},
clientInstance: newTestInstance(),
clientInstanceMutator: func(i *computepb.Instance) { i.NetworkInterfaces[0] = nil },
expectedInstance: core.Instance{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
IPs: []string{},
SSHKeys: map[string][]string{},
},
},
"network ip is nil": {
client: stubInstancesClient{},
clientInstance: newTestInstance(),
clientInstanceMutator: func(i *computepb.Instance) { i.NetworkInterfaces[0].NetworkIP = nil },
expectedInstance: core.Instance{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
IPs: []string{},
SSHKeys: map[string][]string{},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
if tc.clientInstanceMutator != nil {
tc.clientInstanceMutator(tc.clientInstance)
}
tc.client.GetInstance = tc.clientInstance
client := Client{instanceAPI: tc.client}
instance, err := client.RetrieveInstance(context.Background(), "someProject", "someZone", "someInstance")
if tc.expectErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.expectedInstance, instance)
})
}
}
func TestRetrieveProjectID(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
client stubMetadataClient
expectedValue string
expectErr bool
}{
"retrieve works": {
client: stubMetadataClient{ProjectIDValue: "someProjectID"},
expectedValue: "someProjectID",
},
"retrieve fails": {
client: stubMetadataClient{ProjectIDErr: someErr},
expectErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{metadataAPI: tc.client}
value, err := client.RetrieveProjectID()
if tc.expectErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.expectedValue, value)
})
}
}
func TestRetrieveZone(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
client stubMetadataClient
expectedValue string
expectErr bool
}{
"retrieve works": {
client: stubMetadataClient{ZoneValue: "someZone"},
expectedValue: "someZone",
},
"retrieve fails": {
client: stubMetadataClient{ZoneErr: someErr},
expectErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{metadataAPI: tc.client}
value, err := client.RetrieveZone()
if tc.expectErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.expectedValue, value)
})
}
}
func TestRetrieveInstanceName(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
client stubMetadataClient
expectedValue string
expectErr bool
}{
"retrieve works": {
client: stubMetadataClient{InstanceNameValue: "someInstanceName"},
expectedValue: "someInstanceName",
},
"retrieve fails": {
client: stubMetadataClient{InstanceNameErr: someErr},
expectErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{metadataAPI: tc.client}
value, err := client.RetrieveInstanceName()
if tc.expectErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.expectedValue, value)
})
}
}
func TestRetrieveInstanceMetadata(t *testing.T) {
someErr := errors.New("failed")
attr := "someAttribute"
testCases := map[string]struct {
client stubMetadataClient
attr string
expectedValue string
expectErr bool
}{
"retrieve works": {
client: stubMetadataClient{
InstanceValue: "someValue",
InstanceErr: nil,
},
expectedValue: "someValue",
},
"retrieve fails": {
client: stubMetadataClient{
InstanceValue: "",
InstanceErr: someErr,
},
expectErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{metadataAPI: tc.client}
value, err := client.RetrieveInstanceMetadata(attr)
if tc.expectErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.expectedValue, value)
})
}
}
func TestSetInstanceMetadata(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
client stubInstancesClient
expectErr bool
}{
"set works": {
client: stubInstancesClient{
GetInstance: &computepb.Instance{
Metadata: &computepb.Metadata{
Fingerprint: proto.String("someFingerprint"),
Kind: proto.String("compute#metadata"),
Items: []*computepb.Items{},
},
},
},
},
"retrieve fails": {
client: stubInstancesClient{
GetErr: someErr,
},
expectErr: true,
},
"retrieve returns nil": {
expectErr: true,
},
"setting fails": {
client: stubInstancesClient{
GetInstance: &computepb.Instance{
Metadata: &computepb.Metadata{
Fingerprint: proto.String("someFingerprint"),
Kind: proto.String("compute#metadata"),
Items: []*computepb.Items{},
},
},
SetMetadataErr: someErr,
},
expectErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{instanceAPI: tc.client}
err := client.SetInstanceMetadata(context.Background(), "project", "zone", "instanceName", "key", "value")
if tc.expectErr {
assert.Error(err)
return
}
require.NoError(err)
})
}
}
func TestUnsetInstanceMetadata(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
client stubInstancesClient
expectErr bool
}{
"unset works": {
client: stubInstancesClient{
GetInstance: &computepb.Instance{
Metadata: &computepb.Metadata{
Fingerprint: proto.String("someFingerprint"),
Kind: proto.String("compute#metadata"),
Items: []*computepb.Items{},
},
},
},
},
"unset with existing key works": {
client: stubInstancesClient{
GetInstance: &computepb.Instance{
Metadata: &computepb.Metadata{
Fingerprint: proto.String("someFingerprint"),
Kind: proto.String("compute#metadata"),
Items: []*computepb.Items{
{
Key: proto.String("key"),
Value: proto.String("value"),
},
},
},
},
},
},
"retrieve fails": {
client: stubInstancesClient{GetErr: someErr},
expectErr: true,
},
"retrieve returns nil": {
expectErr: true,
},
"setting fails": {
client: stubInstancesClient{
GetInstance: &computepb.Instance{
Metadata: &computepb.Metadata{
Fingerprint: proto.String("someFingerprint"),
Kind: proto.String("compute#metadata"),
Items: []*computepb.Items{},
},
},
SetMetadataErr: someErr,
},
expectErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{instanceAPI: tc.client}
err := client.UnsetInstanceMetadata(context.Background(), "project", "zone", "instanceName", "key")
if tc.expectErr {
assert.Error(err)
return
}
require.NoError(err)
})
}
}
func TestClose(t *testing.T) {
someErr := errors.New("failed")
assert := assert.New(t)
client := Client{instanceAPI: stubInstancesClient{}}
assert.NoError(client.Close())
client = Client{instanceAPI: stubInstancesClient{CloseErr: someErr}}
assert.Error(client.Close())
}
func TestFetchSSHKeys(t *testing.T) {
testCases := map[string]struct {
metadata map[string]string
expectedKeys map[string][]string
}{
"fetch works": {
metadata: map[string]string{"ssh-keys": "bob:ssh-rsa bobskey"},
expectedKeys: map[string][]string{"bob": {"ssh-rsa bobskey"}},
},
"google ssh key metadata is ignored": {
metadata: map[string]string{"ssh-keys": "bob:ssh-rsa bobskey google-ssh {\"userName\":\"bob\",\"expireOn\":\"2021-06-14T16:59:03+0000\"}"},
expectedKeys: map[string][]string{"bob": {"ssh-rsa bobskey"}},
},
"ssh key format error is ignored": {
metadata: map[string]string{"ssh-keys": "incorrect-format"},
expectedKeys: map[string][]string{},
},
"ssh key format space error is ignored": {
metadata: map[string]string{"ssh-keys": "user:incorrect-key-format"},
expectedKeys: map[string][]string{},
},
"metadata field empty": {
metadata: map[string]string{"ssh-keys": ""},
expectedKeys: map[string][]string{},
},
"metadata field missing": {
metadata: map[string]string{},
expectedKeys: map[string][]string{},
},
"multiple keys": {
metadata: map[string]string{"ssh-keys": "bob:ssh-rsa bobskey\nalice:ssh-rsa alicekey"},
expectedKeys: map[string][]string{
"bob": {"ssh-rsa bobskey"},
"alice": {"ssh-rsa alicekey"},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
keys := extractSSHKeys(tc.metadata)
assert.Equal(tc.expectedKeys, keys)
})
}
}
type stubInstanceIterator struct {
instances []*computepb.Instance
nextErr error
internalCounter int
}
func (i *stubInstanceIterator) Next() (*computepb.Instance, error) {
if i.nextErr != nil {
return nil, i.nextErr
}
if i.internalCounter >= len(i.instances) {
i.internalCounter = 0
return nil, iterator.Done
}
resp := i.instances[i.internalCounter]
i.internalCounter++
return resp, nil
}
type stubInstancesClient struct {
GetInstance *computepb.Instance
GetErr error
ListInstanceIterator InstanceIterator
SetMetadataOperation *compute.Operation
SetMetadataErr error
CloseErr error
}
func (s stubInstancesClient) Get(ctx context.Context, req *computepb.GetInstanceRequest, opts ...gax.CallOption) (*computepb.Instance, error) {
return s.GetInstance, s.GetErr
}
func (s stubInstancesClient) List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) InstanceIterator {
return s.ListInstanceIterator
}
func (s stubInstancesClient) SetMetadata(ctx context.Context, req *computepb.SetMetadataInstanceRequest, opts ...gax.CallOption) (*compute.Operation, error) {
return s.SetMetadataOperation, s.SetMetadataErr
}
func (s stubInstancesClient) Close() error {
return s.CloseErr
}
type stubMetadataClient struct {
InstanceValue string
InstanceErr error
ProjectIDValue string
ProjectIDErr error
ZoneValue string
ZoneErr error
InstanceNameValue string
InstanceNameErr error
}
func (s stubMetadataClient) InstanceAttributeValue(attr string) (string, error) {
return s.InstanceValue, s.InstanceErr
}
func (s stubMetadataClient) ProjectID() (string, error) {
return s.ProjectIDValue, s.ProjectIDErr
}
func (s stubMetadataClient) Zone() (string, error) {
return s.ZoneValue, s.ZoneErr
}
func (s stubMetadataClient) InstanceName() (string, error) {
return s.InstanceNameValue, s.InstanceNameErr
}