/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package gcp import ( "context" "errors" "testing" "cloud.google.com/go/compute/apiv1/computepb" "github.com/edgelesssys/constellation/v2/internal/cloud" "github.com/edgelesssys/constellation/v2/internal/cloud/metadata" "github.com/edgelesssys/constellation/v2/internal/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" "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"), ) } func TestGetInstance(t *testing.T) { someErr := errors.New("failed") goodInstance := &computepb.Instance{ Name: proto.String("someInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ cloud.TagUID: "1234", cloud.TagRole: role.ControlPlane.String(), cloud.TagInitSecretHash: "initSecretHash", }, Metadata: &computepb.Metadata{ Items: []*computepb.Items{ { Key: proto.String(cloud.TagInitSecretHash), Value: proto.String("initSecretHash"), }, }, }, NetworkInterfaces: []*computepb.NetworkInterface{ { Name: proto.String("nic0"), NetworkIP: proto.String("192.0.2.0"), AliasIpRanges: []*computepb.AliasIpRange{ { IpCidrRange: proto.String("192.0.3.0/8"), }, }, Subnetwork: proto.String("projects/someProject/regions/someRegion/subnetworks/someSubnetwork"), }, }, } testCases := map[string]struct { projectID, instanceName, zone string instanceAPI stubInstanceAPI subnetAPI stubSubnetAPI wantErr bool wantInstance metadata.InstanceMetadata }{ "success": { instanceName: "someInstance", projectID: "someProject", zone: "someZone-west3-b", instanceAPI: stubInstanceAPI{ instance: goodInstance, }, subnetAPI: stubSubnetAPI{ subnet: &computepb.Subnetwork{ SecondaryIpRanges: []*computepb.SubnetworkSecondaryRange{ { IpCidrRange: proto.String("198.51.100.0/24"), }, }, }, }, wantInstance: metadata.InstanceMetadata{ Name: "someInstance", Role: role.ControlPlane, ProviderID: "gce://someProject/someZone-west3-b/someInstance", VPCIP: "192.0.2.0", AliasIPRanges: []string{"192.0.3.0/8"}, SecondaryIPRange: "198.51.100.0/24", }, }, "get instance error": { instanceName: "someInstance", projectID: "someProject", zone: "someZone-west3-b", instanceAPI: stubInstanceAPI{ instanceErr: someErr, }, subnetAPI: stubSubnetAPI{ subnet: &computepb.Subnetwork{ SecondaryIpRanges: []*computepb.SubnetworkSecondaryRange{ { IpCidrRange: proto.String("198.51.100.0/24"), }, }, }, }, wantErr: true, }, "get subnet error": { instanceName: "someInstance", projectID: "someProject", zone: "someZone-west3-b", instanceAPI: stubInstanceAPI{ instance: goodInstance, }, subnetAPI: stubSubnetAPI{ subnetErr: someErr, }, wantErr: true, }, "invalid instance": { instanceName: "someInstance", projectID: "someProject", zone: "someZone-west3-b", instanceAPI: stubInstanceAPI{ instance: nil, }, subnetAPI: stubSubnetAPI{ subnet: &computepb.Subnetwork{ SecondaryIpRanges: []*computepb.SubnetworkSecondaryRange{ { IpCidrRange: proto.String("198.51.100.0/24"), }, }, }, }, wantErr: true, }, "invalid zone": { instanceName: "someInstance", projectID: "someProject", zone: "invalidZone", instanceAPI: stubInstanceAPI{ instance: goodInstance, }, subnetAPI: stubSubnetAPI{ subnet: &computepb.Subnetwork{ SecondaryIpRanges: []*computepb.SubnetworkSecondaryRange{ { IpCidrRange: proto.String("198.51.100.0/24"), }, }, }, }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) require := require.New(t) cloud := &Cloud{ instanceAPI: &tc.instanceAPI, subnetAPI: &tc.subnetAPI, } instance, err := cloud.getInstance(context.Background(), tc.projectID, tc.zone, tc.instanceName) if tc.wantErr { assert.Error(err) return } require.NoError(err) assert.Equal(tc.wantInstance, instance) }) } } func TestGetLoadbalancerEndpoint(t *testing.T) { someErr := errors.New("failed") goodInstance := &computepb.Instance{ Name: proto.String("someInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ cloud.TagUID: "1234", cloud.TagRole: role.ControlPlane.String(), }, NetworkInterfaces: []*computepb.NetworkInterface{ { Name: proto.String("nic0"), NetworkIP: proto.String("192.0.2.0"), AliasIpRanges: []*computepb.AliasIpRange{ { IpCidrRange: proto.String("192.0.3.0/8"), }, }, Subnetwork: proto.String("projects/someProject/regions/someRegion/subnetworks/someSubnetwork"), }, }, } testCases := map[string]struct { imds stubIMDS instanceAPI stubInstanceAPI forwardingRulesAPI stubForwardingRulesAPI wantEndpoint string wantErr bool }{ "success": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, }, forwardingRulesAPI: stubForwardingRulesAPI{ iterator: &stubForwardingRulesIterator{ forwardingRules: []*computepb.ForwardingRule{ { PortRange: proto.String("6443"), IPAddress: proto.String("192.0.2.255"), }, }, }, }, wantEndpoint: "192.0.2.255:6443", }, "imds error": { imds: stubIMDS{ projectIDErr: someErr, zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, }, forwardingRulesAPI: stubForwardingRulesAPI{ iterator: &stubForwardingRulesIterator{ forwardingRules: []*computepb.ForwardingRule{ { PortRange: proto.String("6443"), IPAddress: proto.String("192.0.2.255"), }, }, }, }, wantErr: true, }, "iterator error": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, }, forwardingRulesAPI: stubForwardingRulesAPI{ iterator: &stubForwardingRulesIterator{ err: someErr, }, }, wantErr: true, }, "no forwarding rules": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, }, forwardingRulesAPI: stubForwardingRulesAPI{ iterator: &stubForwardingRulesIterator{}, }, wantErr: true, }, "missing port range": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, }, forwardingRulesAPI: stubForwardingRulesAPI{ iterator: &stubForwardingRulesIterator{ forwardingRules: []*computepb.ForwardingRule{ { IPAddress: proto.String("192.0.2.255"), }, }, }, }, wantErr: true, }, "missing IP address": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, }, forwardingRulesAPI: stubForwardingRulesAPI{ iterator: &stubForwardingRulesIterator{ forwardingRules: []*computepb.ForwardingRule{ { PortRange: proto.String("6443"), }, }, }, }, wantErr: true, }, "get instance error": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instanceErr: someErr, }, forwardingRulesAPI: stubForwardingRulesAPI{ iterator: &stubForwardingRulesIterator{ forwardingRules: []*computepb.ForwardingRule{ { PortRange: proto.String("6443"), IPAddress: proto.String("192.0.2.255"), }, }, }, }, wantErr: true, }, "invalid instance": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: nil, }, forwardingRulesAPI: stubForwardingRulesAPI{ iterator: &stubForwardingRulesIterator{ forwardingRules: []*computepb.ForwardingRule{ { PortRange: proto.String("6443"), IPAddress: proto.String("192.0.2.255"), }, }, }, }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) cloud := &Cloud{ imds: &tc.imds, instanceAPI: &tc.instanceAPI, forwardingRulesAPI: &tc.forwardingRulesAPI, } endpoint, err := cloud.GetLoadBalancerEndpoint(context.Background()) if tc.wantErr { assert.Error(err) return } assert.NoError(err) assert.Equal(tc.wantEndpoint, endpoint) }) } } func TestList(t *testing.T) { someErr := errors.New("failed") goodInstance := &computepb.Instance{ Name: proto.String("someInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ cloud.TagUID: "1234", cloud.TagRole: role.ControlPlane.String(), }, NetworkInterfaces: []*computepb.NetworkInterface{ { Name: proto.String("nic0"), NetworkIP: proto.String("192.0.2.0"), AliasIpRanges: []*computepb.AliasIpRange{ { IpCidrRange: proto.String("198.51.100.0/24"), }, }, Subnetwork: proto.String("projects/someProject/regions/someRegion/subnetworks/someSubnetwork"), }, }, } goodSubnet := &computepb.Subnetwork{ SecondaryIpRanges: []*computepb.SubnetworkSecondaryRange{ { IpCidrRange: proto.String("198.51.100.0/24"), }, }, } testCases := map[string]struct { imds stubIMDS instanceAPI stubInstanceAPI subnetAPI stubSubnetAPI wantErr bool wantInstances []metadata.InstanceMetadata }{ "success": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, iterator: &stubInstanceIterator{ instances: []*computepb.Instance{ goodInstance, }, }, }, subnetAPI: stubSubnetAPI{ subnet: goodSubnet, }, wantInstances: []metadata.InstanceMetadata{ { Name: "someInstance", Role: role.ControlPlane, ProviderID: "gce://someProject/someZone-west3-b/someInstance", VPCIP: "192.0.2.0", AliasIPRanges: []string{"198.51.100.0/24"}, SecondaryIPRange: "198.51.100.0/24", }, }, }, "list multiple instances": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, iterator: &stubInstanceIterator{ instances: []*computepb.Instance{ goodInstance, { Name: proto.String("anotherInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ cloud.TagUID: "1234", cloud.TagRole: role.Worker.String(), }, NetworkInterfaces: []*computepb.NetworkInterface{ { Name: proto.String("nic0"), NetworkIP: proto.String("192.0.2.1"), AliasIpRanges: []*computepb.AliasIpRange{ { IpCidrRange: proto.String("198.51.100.0/24"), }, }, Subnetwork: proto.String("projects/someProject/regions/someRegion/subnetworks/someSubnetwork"), }, }, }, }, }, }, subnetAPI: stubSubnetAPI{ subnet: goodSubnet, }, wantInstances: []metadata.InstanceMetadata{ { Name: "someInstance", Role: role.ControlPlane, ProviderID: "gce://someProject/someZone-west3-b/someInstance", VPCIP: "192.0.2.0", AliasIPRanges: []string{"198.51.100.0/24"}, SecondaryIPRange: "198.51.100.0/24", }, { Name: "anotherInstance", Role: role.Worker, ProviderID: "gce://someProject/someZone-west3-b/anotherInstance", VPCIP: "192.0.2.1", AliasIPRanges: []string{"198.51.100.0/24"}, SecondaryIPRange: "198.51.100.0/24", }, }, }, "imds error": { imds: stubIMDS{ projectIDErr: someErr, }, instanceAPI: stubInstanceAPI{ instance: goodInstance, iterator: &stubInstanceIterator{ instances: []*computepb.Instance{ goodInstance, }, }, }, subnetAPI: stubSubnetAPI{ subnet: goodSubnet, }, wantErr: true, }, "iterator error": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, iterator: &stubInstanceIterator{ err: someErr, }, }, subnetAPI: stubSubnetAPI{ subnet: goodSubnet, }, wantErr: true, }, "get instance error": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instanceErr: someErr, iterator: &stubInstanceIterator{ instances: []*computepb.Instance{ goodInstance, }, }, }, subnetAPI: stubSubnetAPI{ subnet: goodSubnet, }, wantErr: true, }, "get subnet error": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: goodInstance, iterator: &stubInstanceIterator{ instances: []*computepb.Instance{ goodInstance, }, }, }, subnetAPI: stubSubnetAPI{ subnetErr: someErr, }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) require := require.New(t) cloud := &Cloud{ imds: &tc.imds, instanceAPI: &tc.instanceAPI, subnetAPI: &tc.subnetAPI, } instances, err := cloud.List(context.Background()) if tc.wantErr { assert.Error(err) return } require.NoError(err) assert.ElementsMatch(tc.wantInstances, instances) }) } } func TestRetrieveInstanceInfo(t *testing.T) { someErr := errors.New("failed") testCases := map[string]struct { imds stubIMDS wantErr bool }{ "success": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, }, "get project id error": { imds: stubIMDS{ zone: "someZone-west3-b", instanceName: "someInstance", projectIDErr: someErr, }, wantErr: true, }, "get zone error": { imds: stubIMDS{ projectID: "someProject", instanceName: "someInstance", zoneErr: someErr, }, wantErr: true, }, "get instance name error": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceNameErr: someErr, }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) cloud := &Cloud{ imds: &tc.imds, } project, zone, instance, err := cloud.retrieveInstanceInfo() if tc.wantErr { assert.Error(err) return } assert.NoError(err) assert.Equal(tc.imds.projectID, project) assert.Equal(tc.imds.zone, zone) assert.Equal(tc.imds.instanceName, instance) }) } } func TestUID(t *testing.T) { someErr := errors.New("failed") testCases := map[string]struct { imds stubIMDS instanceAPI stubInstanceAPI wantUID string wantErr bool }{ "success": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: &computepb.Instance{ Name: proto.String("someInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ cloud.TagUID: "1234", cloud.TagRole: role.ControlPlane.String(), }, }, }, wantUID: "1234", }, "imds error": { imds: stubIMDS{ projectIDErr: someErr, zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: &computepb.Instance{ Name: proto.String("someInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ cloud.TagUID: "1234", cloud.TagRole: role.ControlPlane.String(), }, }, }, wantErr: true, }, "instance error": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instanceErr: someErr, }, wantErr: true, }, "invalid instance": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: nil, }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) cloud := &Cloud{ imds: &tc.imds, instanceAPI: &tc.instanceAPI, } uid, err := cloud.UID(context.Background()) if tc.wantErr { assert.Error(err) return } assert.NoError(err) assert.Equal(tc.wantUID, uid) }) } } func TestInitSecretHash(t *testing.T) { someErr := errors.New("failed") testCases := map[string]struct { imds stubIMDS instanceAPI stubInstanceAPI wantInitSecretHash string wantErr bool }{ "success": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: &computepb.Instance{ Name: proto.String("someInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ cloud.TagRole: role.ControlPlane.String(), }, Metadata: &computepb.Metadata{ Items: []*computepb.Items{ { Key: proto.String(cloud.TagInitSecretHash), Value: proto.String("initSecretHash"), }, }, }, }, }, wantInitSecretHash: "initSecretHash", }, "imds error": { imds: stubIMDS{ projectIDErr: someErr, zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: &computepb.Instance{ Name: proto.String("someInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ cloud.TagInitSecretHash: "initSecretHash", cloud.TagRole: role.ControlPlane.String(), }, Metadata: &computepb.Metadata{ Items: []*computepb.Items{ { Key: proto.String(cloud.TagInitSecretHash), Value: proto.String("initSecretHash"), }, }, }, }, }, wantErr: true, }, "instance error": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instanceErr: someErr, }, wantErr: true, }, "invalid instance": { imds: stubIMDS{ projectID: "someProject", zone: "someZone-west3-b", instanceName: "someInstance", }, instanceAPI: stubInstanceAPI{ instance: nil, }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) cloud := &Cloud{ imds: &tc.imds, instanceAPI: &tc.instanceAPI, } initSecretHash, err := cloud.InitSecretHash(context.Background()) if tc.wantErr { assert.Error(err) return } assert.NoError(err) assert.Equal([]byte(tc.wantInitSecretHash), initSecretHash) }) } } type stubForwardingRulesAPI struct { iterator forwardingRuleIterator } func (s *stubForwardingRulesAPI) List( ctx context.Context, req *computepb.ListGlobalForwardingRulesRequest, opts ...gax.CallOption, ) forwardingRuleIterator { return s.iterator } func (s *stubForwardingRulesAPI) Close() error { return nil } type stubForwardingRulesIterator struct { ctr int forwardingRules []*computepb.ForwardingRule err error } func (s *stubForwardingRulesIterator) Next() (*computepb.ForwardingRule, error) { if s.err != nil { return nil, s.err } if s.ctr >= len(s.forwardingRules) { return nil, iterator.Done } s.ctr++ return s.forwardingRules[s.ctr-1], nil } type stubIMDS struct { instanceID string projectID string zone string instanceName string instanceIDErr error projectIDErr error zoneErr error instanceNameErr error } func (s *stubIMDS) InstanceID() (string, error) { return s.instanceID, s.instanceIDErr } func (s *stubIMDS) ProjectID() (string, error) { return s.projectID, s.projectIDErr } func (s *stubIMDS) Zone() (string, error) { return s.zone, s.zoneErr } func (s *stubIMDS) InstanceName() (string, error) { return s.instanceName, s.instanceNameErr } type stubInstanceAPI struct { instance *computepb.Instance instanceErr error iterator *stubInstanceIterator } func (s *stubInstanceAPI) Get( ctx context.Context, req *computepb.GetInstanceRequest, opts ...gax.CallOption, ) (*computepb.Instance, error) { return s.instance, s.instanceErr } func (s *stubInstanceAPI) List( ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption, ) instanceIterator { return s.iterator } func (s *stubInstanceAPI) Close() error { return nil } type stubInstanceIterator struct { ctr int instances []*computepb.Instance err error } func (s *stubInstanceIterator) Next() (*computepb.Instance, error) { if s.err != nil { return nil, s.err } if s.ctr >= len(s.instances) { return nil, iterator.Done } s.ctr++ return s.instances[s.ctr-1], nil } type stubSubnetAPI struct { subnet *computepb.Subnetwork subnetErr error } func (s *stubSubnetAPI) Get( ctx context.Context, req *computepb.GetSubnetworkRequest, opts ...gax.CallOption, ) (*computepb.Subnetwork, error) { return s.subnet, s.subnetErr } func (s *stubSubnetAPI) Close() error { return nil }