/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package openstack import ( "context" "errors" "fmt" "testing" "github.com/edgelesssys/constellation/v2/internal/cloud/metadata" "github.com/edgelesssys/constellation/v2/internal/role" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" "github.com/gophercloud/gophercloud/pagination" "github.com/stretchr/testify/assert" ) func TestSelf(t *testing.T) { someErr := fmt.Errorf("failed") testCases := map[string]struct { imds imdsAPI want metadata.InstanceMetadata wantErr bool }{ "success": { imds: &stubIMDSClient{ nameResult: "name", providerIDResult: "providerID", roleResult: role.ControlPlane, vpcIPResult: "192.0.2.1", }, want: metadata.InstanceMetadata{ Name: "name", ProviderID: "providerID", Role: role.ControlPlane, VPCIP: "192.0.2.1", }, }, "fail to get name": { imds: &stubIMDSClient{ nameErr: someErr, providerIDResult: "providerID", roleResult: role.ControlPlane, vpcIPResult: "192.0.2.1", }, wantErr: true, }, "fail to get provider ID": { imds: &stubIMDSClient{ nameResult: "name", providerIDErr: someErr, roleResult: role.ControlPlane, vpcIPResult: "192.0.2.1", }, wantErr: true, }, "fail to get role": { imds: &stubIMDSClient{ nameResult: "name", providerIDResult: "providerID", roleErr: someErr, vpcIPResult: "192.0.2.1", }, wantErr: true, }, "fail to get VPC IP": { imds: &stubIMDSClient{ nameResult: "name", providerIDResult: "providerID", roleResult: role.ControlPlane, vpcIPErr: someErr, }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) c := &Cloud{imds: tc.imds} got, err := c.Self(context.Background()) if tc.wantErr { assert.Error(err) } else { assert.NoError(err) assert.Equal(tc.want, got) } }) } } func TestList(t *testing.T) { someErr := fmt.Errorf("failed") // newTestAddrs returns a set of raw server addresses as we would get from // a ListServers call and as expected by the parseSeverAddresses function. // The hardcoded addresses don't match what we are looking for. A valid // address can be injected. You can pass a second valid address to test // that the first valid one is chosen. newTestAddrs := func(vpcIP1, vpcIP2 string) map[string]any { return map[string]any{ "network1": []any{ map[string]any{ "addr": "198.51.100.0", "version": 4, "OS-EXT-IPS:type": "fixed", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, }, "network2": []any{ map[string]any{ "addr": "192.0.2.1", "version": 4, "OS-EXT-IPS:type": "floating", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, map[string]any{ "addr": "2001:db8:3333:4444:5555:6666:7777:8888", "version": 6, "OS-EXT-IPS:type": "fixed", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, map[string]any{ "addr": vpcIP1, "version": 4, "OS-EXT-IPS:type": "fixed", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, map[string]any{ "addr": vpcIP2, "version": 4, "OS-EXT-IPS:type": "fixed", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, }, } } testCases := map[string]struct { imds imdsAPI api serversAPI want []metadata.InstanceMetadata wantErr bool }{ "success": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("192.0.2.5", ""), }, { Name: "name2", ID: "id2", Tags: &[]string{"constellation-role-worker", "constellation-uid-7777"}, Addresses: newTestAddrs("192.0.2.6", "192.0.2.99"), }, { Name: "name3", ID: "id3", Tags: &[]string{"constellation-role-worker", "constellation-uid-8888"}, Addresses: newTestAddrs("198.51.100.1", ""), }, }, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, want: []metadata.InstanceMetadata{ { Name: "name1", ProviderID: "id1", Role: role.ControlPlane, VPCIP: "192.0.2.5", }, { Name: "name2", ProviderID: "id2", Role: role.Worker, VPCIP: "192.0.2.6", }, }, }, "no servers found": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{}, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "imds uid error": { imds: &stubIMDSClient{uidErr: someErr}, wantErr: true, }, "list subnets error": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ subnetsPager: stubPager{allPagesErr: someErr}, }, wantErr: true, }, "extract subnets error": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{}, someErr), }, wantErr: true, }, "multiple subnets error": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{ {CIDR: "192.0.2.0/24"}, {CIDR: "198.51.100.0/24"}, }, nil), }, wantErr: true, }, "parse subnet error": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "notAnIP"}}, nil), }, wantErr: true, }, "list servers error": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: stubPager{allPagesErr: someErr}, subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "extract servers error": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{}, someErr), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "sever with empty name skipped": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{ { ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("192.0.2.5", ""), }, }, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "sever with nil tags skipped": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Addresses: newTestAddrs("192.0.2.5", ""), }, }, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "server with empty ID skipped": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{ { Name: "name1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("192.0.2.5", ""), }, }, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "server with unknown role skipped": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-unknown", "constellation-uid-7777"}, Addresses: newTestAddrs("192.0.2.5", ""), }, }, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "server without role skipped": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-uid-7777"}, Addresses: newTestAddrs("192.0.2.5", ""), }, }, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "server without parseable addresses skipped": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: map[string]any{"foo": "bar"}, }, }, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, "server addresses contains in": { imds: &stubIMDSClient{uidResult: "uid"}, api: &stubServersClient{ serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("invalidIP", ""), }, }, nil), subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) c := &Cloud{imds: tc.imds, api: tc.api} got, err := c.List(context.Background()) if tc.wantErr { assert.Error(err) } else { assert.NoError(err) assert.Equal(tc.want, got) } }) } } func TestUID(t *testing.T) { testCases := map[string]struct { imds *stubIMDSClient want string wantErr bool }{ "error returned from IMDS client": { imds: &stubIMDSClient{uidErr: errors.New("failed")}, wantErr: true, }, "UID returned from IMDS client": { imds: &stubIMDSClient{uidResult: "uid"}, want: "uid", }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) c := &Cloud{imds: tc.imds} got, err := c.UID(context.Background()) if tc.wantErr { assert.Error(err) } else { assert.NoError(err) assert.Equal(tc.want, got) } }) } } func TestInitSecretHash(t *testing.T) { testCases := map[string]struct { imds *stubIMDSClient want []byte wantErr bool }{ "error returned from IMDS client": { imds: &stubIMDSClient{initSecretHashErr: errors.New("failed")}, wantErr: true, }, "initSecretHash returned from IMDS client": { imds: &stubIMDSClient{initSecretHashResult: "initSecretHash"}, want: []byte("initSecretHash"), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) c := &Cloud{imds: tc.imds} got, err := c.InitSecretHash(context.Background()) if tc.wantErr { assert.Error(err) } else { assert.NoError(err) assert.Equal(tc.want, got) } }) } } func TestGetLoadBalancerEndpoint(t *testing.T) { // newTestAddrs returns a set of raw server addresses as we would get from // a ListServers call and as expected by the parseSeverAddresses function. // The hardcoded addresses don't match what we are looking for. A valid // address can be injected. You can pass a second valid address to test // that the first valid one is chosen. newTestAddrs := func(floatingIP1, floatingIP2 string, fixedIP1 string) map[string]any { return map[string]any{ "network1": []any{ map[string]any{ "addr": "192.0.2.2", "version": 4, "OS-EXT-IPS:type": "floating", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, }, "network2": []any{ map[string]any{ "addr": fixedIP1, "version": 4, "OS-EXT-IPS:type": "fixed", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, map[string]any{ "addr": "2001:db8:3333:4444:5555:6666:7777:8888", "version": 6, "OS-EXT-IPS:type": "floating", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, map[string]any{ "addr": floatingIP1, "version": 4, "OS-EXT-IPS:type": "floating", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, map[string]any{ "addr": floatingIP2, "version": 4, "OS-EXT-IPS:type": "floating", "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c", }, }, } } testCases := map[string]struct { imds *stubIMDSClient api *stubServersClient wantHost string wantErr bool }{ "error returned from IMDS client": { imds: &stubIMDSClient{uidErr: errors.New("failed")}, wantErr: true, }, "error returned from getSubnetCIDR": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager(nil, errors.New("failed")), }, wantErr: true, }, "error returned from getServers": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager(nil, errors.New("failed")), }, wantErr: true, }, "sever with empty name skipped": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager([]servers.Server{ { ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("198.51.100.0", "", "192.0.2.1"), }, }, nil), }, wantErr: true, }, "server with empty ID skipped": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager([]servers.Server{ { Name: "name1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("198.51.100.0", "", "192.0.2.1"), }, }, nil), }, wantErr: true, }, "sever with nil tags skipped": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Addresses: newTestAddrs("198.51.100.0", "", "192.0.2.1"), }, }, nil), }, wantErr: true, }, "server has invalid address": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("", "", "invalidIP"), }, }, nil), }, wantErr: true, }, "server without parseable addresses skipped": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: map[string]any{ "somekey": "invalid", }, }, }, nil), }, wantErr: true, }, "invalid endpoint returned from server addresses": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("invalidIP", "", "192.0.2.1"), }, }, nil), }, wantErr: true, }, "valid endpoint returned from server addresses not in subnet CIDR": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("198.51.100.0", "", "192.0.2.1"), }, }, nil), }, wantHost: "198.51.100.0", }, "first valid endpoint returned from server addresses not in subnet CIDR": { imds: &stubIMDSClient{}, api: &stubServersClient{ subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil), serversPager: newSeverPager([]servers.Server{ { Name: "name1", ID: "id1", Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"}, Addresses: newTestAddrs("198.51.100.0", "198.51.100.1", "192.0.2.1"), }, }, nil), }, wantHost: "198.51.100.0", }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) c := &Cloud{ imds: tc.imds, api: tc.api, } gotHost, gotPort, err := c.GetLoadBalancerEndpoint(context.Background()) if tc.wantErr { assert.Error(err) } else { assert.NoError(err) assert.Equal(tc.wantHost, gotHost) assert.Equal("6443", gotPort) } }) } } func TestGetNetworkIDs(t *testing.T) { someErr := fmt.Errorf("failed") testCases := map[string]struct { imds imdsAPI want []string wantErr bool }{ "success": { imds: &stubIMDSClient{ networkIDsResult: []string{"id1", "id2"}, }, want: []string{"id1", "id2"}, }, "fail to get network IDs": { imds: &stubIMDSClient{ networkIDsErr: someErr, }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) c := &Cloud{imds: tc.imds} got, err := c.GetNetworkIDs(context.Background()) if tc.wantErr { assert.Error(err) } else { assert.NoError(err) assert.Equal(tc.want, got) } }) } } // newSubnetPager returns a subnet pager as we would get from a ListSubnets. func newSubnetPager(nets []subnets.Subnet, err error) stubPager { return stubPager{ page: subnets.SubnetPage{ LinkedPageBase: pagination.LinkedPageBase{ PageResult: pagination.PageResult{ Result: gophercloud.Result{ Body: struct { Subnets []subnets.Subnet `json:"subnets"` }{nets}, Err: err, }, }, }, }, } } // newSeverPager returns a server pager as we would get from a ListServers. func newSeverPager(srvs []servers.Server, err error) stubPager { return stubPager{ page: servers.ServerPage{ LinkedPageBase: pagination.LinkedPageBase{ PageResult: pagination.PageResult{ Result: gophercloud.Result{ Body: struct { Servers []servers.Server `json:"servers"` }{srvs}, Err: err, }, }, }, }, } }