mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-24 06:59:40 -05:00
openstack: implement api client and metadata list
Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
This commit is contained in:
parent
418f08bf40
commit
acbd70c741
2
go.mod
2
go.mod
@ -219,6 +219,8 @@ require (
|
||||
github.com/google/trillian v1.5.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/gophercloud/gophercloud v1.2.0
|
||||
github.com/gophercloud/utils v0.0.0-20230301065655-769528992f29
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
|
6
go.sum
6
go.sum
@ -735,6 +735,11 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
|
||||
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gophercloud/gophercloud v1.1.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
|
||||
github.com/gophercloud/gophercloud v1.2.0 h1:1oXyj4g54KBg/kFtCdMM6jtxSzeIyg8wv4z1HoGPp1E=
|
||||
github.com/gophercloud/gophercloud v1.2.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
|
||||
github.com/gophercloud/utils v0.0.0-20230301065655-769528992f29 h1:A2dT0wlliiBwnkWVPtSLXH1m36486l7sOwPRrjEm3hE=
|
||||
github.com/gophercloud/utils v0.0.0-20230301065655-769528992f29/go.mod h1:z4Dey7xsTUXgcB1C8elMvGRKTjV1ez0eoYQlMrduG1g=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ=
|
||||
@ -1498,6 +1503,7 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
|
@ -10,6 +10,9 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
||||
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
|
||||
"github.com/gophercloud/gophercloud/pagination"
|
||||
)
|
||||
|
||||
type imdsAPI interface {
|
||||
@ -21,3 +24,12 @@ type imdsAPI interface {
|
||||
role(ctx context.Context) (role.Role, error)
|
||||
vpcIP(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type serversAPI interface {
|
||||
ListServers(opts servers.ListOptsBuilder) pagerAPI
|
||||
ListSubnets(opts subnets.ListOpts) pagerAPI
|
||||
}
|
||||
|
||||
type pagerAPI interface {
|
||||
AllPages() (pagination.Page, error)
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
||||
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
|
||||
"github.com/gophercloud/gophercloud/pagination"
|
||||
)
|
||||
|
||||
type stubIMDSClient struct {
|
||||
@ -56,3 +59,25 @@ func (c *stubIMDSClient) role(ctx context.Context) (role.Role, error) {
|
||||
func (c *stubIMDSClient) vpcIP(ctx context.Context) (string, error) {
|
||||
return c.vpcIPResult, c.vpcIPErr
|
||||
}
|
||||
|
||||
type stubServersClient struct {
|
||||
serversPager stubPager
|
||||
subnetsPager stubPager
|
||||
}
|
||||
|
||||
func (c *stubServersClient) ListServers(opts servers.ListOptsBuilder) pagerAPI {
|
||||
return &c.serversPager
|
||||
}
|
||||
|
||||
func (c *stubServersClient) ListSubnets(opts subnets.ListOpts) pagerAPI {
|
||||
return &c.subnetsPager
|
||||
}
|
||||
|
||||
type stubPager struct {
|
||||
page pagination.Page
|
||||
allPagesErr error
|
||||
}
|
||||
|
||||
func (p *stubPager) AllPages() (pagination.Page, error) {
|
||||
return p.page, p.allPagesErr
|
||||
}
|
||||
|
@ -10,19 +10,77 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
|
||||
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
||||
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
|
||||
"github.com/gophercloud/utils/openstack/clientconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
roleTagFormat = "constellation-role-%s"
|
||||
microversion = "2.42"
|
||||
)
|
||||
|
||||
// Cloud is the metadata client for OpenStack.
|
||||
type Cloud struct {
|
||||
api serversAPI
|
||||
imds imdsAPI
|
||||
}
|
||||
|
||||
// New creates a new OpenStack metadata client.
|
||||
func New(ctx context.Context) (*Cloud, error) {
|
||||
imds := &imdsClient{client: &http.Client{}}
|
||||
return &Cloud{imds: imds}, nil
|
||||
|
||||
authURL, err := imds.authURL(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting auth URL: %w", err)
|
||||
}
|
||||
username, err := imds.username(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting token name: %w", err)
|
||||
}
|
||||
password, err := imds.password(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting token password: %w", err)
|
||||
}
|
||||
userDomainName, err := imds.userDomainName(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting user domain name: %w", err)
|
||||
}
|
||||
|
||||
clientOpts := &clientconfig.ClientOpts{
|
||||
AuthType: clientconfig.AuthV3Password,
|
||||
AuthInfo: &clientconfig.AuthInfo{
|
||||
AuthURL: authURL,
|
||||
UserDomainName: userDomainName,
|
||||
Username: username,
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
|
||||
serversClient, err := clientconfig.NewServiceClient("compute", clientOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating compute client: %w", err)
|
||||
}
|
||||
serversClient.Microversion = microversion
|
||||
|
||||
subnetsClient, err := clientconfig.NewServiceClient("network", clientOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating network client: %w", err)
|
||||
}
|
||||
subnetsClient.Microversion = microversion
|
||||
|
||||
return &Cloud{
|
||||
imds: imds,
|
||||
api: &apiClient{
|
||||
servers: serversClient,
|
||||
subnets: subnetsClient,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Self returns the metadata of the current instance.
|
||||
@ -51,3 +109,138 @@ func (c *Cloud) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
||||
VPCIP: vpcIP,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List returns the metadata of all instances belonging to the same Constellation cluster.
|
||||
func (c *Cloud) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
|
||||
uid, err := c.imds.uid(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting uid: %w", err)
|
||||
}
|
||||
|
||||
uidTag := fmt.Sprintf("constellation-uid-%s", uid)
|
||||
|
||||
subnet, err := c.getSubnetCIDR(uidTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srvs, err := c.getServers(uidTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []metadata.InstanceMetadata
|
||||
for _, s := range srvs {
|
||||
if s.Name == "" {
|
||||
continue
|
||||
}
|
||||
if s.ID == "" {
|
||||
continue
|
||||
}
|
||||
if s.Tags == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var serverRole role.Role
|
||||
for _, t := range *s.Tags {
|
||||
if strings.HasPrefix(t, "constellation-role-") {
|
||||
serverRole = role.FromString(strings.TrimPrefix(t, "constellation-role-"))
|
||||
break
|
||||
}
|
||||
}
|
||||
if serverRole == role.Unknown {
|
||||
continue
|
||||
}
|
||||
|
||||
subnetAddrs, err := parseSeverAddresses(s.Addresses)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing server %q addresses: %w", s.Name, err)
|
||||
}
|
||||
|
||||
var vpcIP string
|
||||
// In a best effort approach, we take the first fixed IPv4 address that is in the subnet
|
||||
// belonging to our cluster.
|
||||
for _, serverSubnet := range subnetAddrs {
|
||||
for _, addr := range serverSubnet.Addresses {
|
||||
if addr.Type != fixedIP {
|
||||
continue
|
||||
}
|
||||
|
||||
if addr.IPVersion != ipV4 {
|
||||
continue
|
||||
}
|
||||
|
||||
if addr.IP == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parsedAddr, err := netip.ParseAddr(addr.IP)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !subnet.Contains(parsedAddr) {
|
||||
continue
|
||||
}
|
||||
|
||||
vpcIP = addr.IP
|
||||
break
|
||||
}
|
||||
}
|
||||
if vpcIP == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
im := metadata.InstanceMetadata{
|
||||
Name: s.Name,
|
||||
ProviderID: s.ID,
|
||||
Role: serverRole,
|
||||
VPCIP: vpcIP,
|
||||
}
|
||||
result = append(result, im)
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return nil, fmt.Errorf("no instances belonging to this cluster found")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Cloud) getSubnetCIDR(uidTag string) (netip.Prefix, error) {
|
||||
listSubnetsOpts := subnets.ListOpts{Tags: uidTag}
|
||||
subnetsPage, err := c.api.ListSubnets(listSubnetsOpts).AllPages()
|
||||
if err != nil {
|
||||
return netip.Prefix{}, fmt.Errorf("listing subnets: %w", err)
|
||||
}
|
||||
|
||||
nets, err := subnets.ExtractSubnets(subnetsPage)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, fmt.Errorf("extracting subnets: %w", err)
|
||||
}
|
||||
|
||||
if len(nets) != 1 {
|
||||
return netip.Prefix{}, fmt.Errorf("expected exactly one subnet, got %d", len(nets))
|
||||
}
|
||||
|
||||
cidr, err := netip.ParsePrefix(nets[0].CIDR)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, fmt.Errorf("parsing subnet CIDR: %w", err)
|
||||
}
|
||||
|
||||
return cidr, nil
|
||||
}
|
||||
|
||||
func (c *Cloud) getServers(uidTag string) ([]servers.Server, error) {
|
||||
listServersOpts := servers.ListOpts{Tags: uidTag}
|
||||
serversPage, err := c.api.ListServers(listServersOpts).AllPages()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing servers: %w", err)
|
||||
}
|
||||
servers, err := servers.ExtractServers(serversPage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extracting servers: %w", err)
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
@ -13,6 +13,10 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -93,3 +97,313 @@ func TestSelf(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newSubnetPager returns a subnet pager as we would get from a ListSubnets
|
||||
newSubnetPager := func(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
|
||||
newSeverPager := func(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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
111
internal/cloud/openstack/plumbing.go
Normal file
111
internal/cloud/openstack/plumbing.go
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package openstack
|
||||
|
||||
import "fmt"
|
||||
|
||||
// parseServerAddresses parses the untyped Addresses field of a sever struct.
|
||||
func parseSeverAddresses(addrsMap map[string]any) ([]serverSubnetAddresses, error) {
|
||||
result := []serverSubnetAddresses{}
|
||||
for name, v := range addrsMap {
|
||||
subnet := serverSubnetAddresses{NetworkName: name}
|
||||
|
||||
v, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server address %q is not of form []any", name)
|
||||
}
|
||||
|
||||
for _, v := range v {
|
||||
nic := serverAddress{}
|
||||
|
||||
v, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("element in server address %q is not of form []map[string]any", name)
|
||||
}
|
||||
|
||||
if mac, ok := v["OS-EXT-IPS-MAC:mac_addr"].(string); ok {
|
||||
nic.MAC = mac
|
||||
}
|
||||
|
||||
if ip, ok := v["addr"].(string); ok {
|
||||
nic.IP = ip
|
||||
}
|
||||
|
||||
if ipVersion, ok := v["version"].(float64); ok {
|
||||
version, err := ipVersionFromFloat(ipVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nic.IPVersion = version
|
||||
}
|
||||
|
||||
if typ, ok := v["OS-EXT-IPS:type"].(string); ok {
|
||||
ipType, err := ipTypeFromString(typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nic.Type = ipType
|
||||
}
|
||||
|
||||
subnet.Addresses = append(subnet.Addresses, nic)
|
||||
}
|
||||
|
||||
result = append(result, subnet)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type serverSubnetAddresses struct {
|
||||
NetworkName string
|
||||
Addresses []serverAddress
|
||||
}
|
||||
|
||||
type serverAddress struct {
|
||||
Type ipType
|
||||
IPVersion ipVersion
|
||||
IP string
|
||||
MAC string
|
||||
}
|
||||
|
||||
type ipVersion int
|
||||
|
||||
const (
|
||||
ipVUnknown ipVersion = 0
|
||||
ipV4 ipVersion = 4
|
||||
ipV6 ipVersion = 6
|
||||
)
|
||||
|
||||
func ipVersionFromFloat(f float64) (ipVersion, error) {
|
||||
switch f {
|
||||
case 4:
|
||||
return ipV4, nil
|
||||
case 6:
|
||||
return ipV6, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown IP version %f", f)
|
||||
}
|
||||
}
|
||||
|
||||
type ipType string
|
||||
|
||||
const (
|
||||
unknownIP ipType = "unknown"
|
||||
fixedIP ipType = "fixed"
|
||||
floatingIP ipType = "floating"
|
||||
)
|
||||
|
||||
func ipTypeFromString(s string) (ipType, error) {
|
||||
switch s {
|
||||
case "fixed":
|
||||
return fixedIP, nil
|
||||
case "floating":
|
||||
return floatingIP, nil
|
||||
default:
|
||||
return unknownIP, fmt.Errorf("unknown IP type %s", s)
|
||||
}
|
||||
}
|
143
internal/cloud/openstack/plumbing_test.go
Normal file
143
internal/cloud/openstack/plumbing_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseServerAddresses(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
input string
|
||||
expected []serverSubnetAddresses
|
||||
wantErr bool
|
||||
}{
|
||||
"valid": {
|
||||
input: `
|
||||
{
|
||||
"network1": [
|
||||
{
|
||||
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00",
|
||||
"version": 4,
|
||||
"addr": "192.0.2.1",
|
||||
"OS-EXT-IPS:type": "fixed"
|
||||
},
|
||||
{
|
||||
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:01",
|
||||
"version": 6,
|
||||
"addr": "2001:db8:3333:4444:5555:6666:7777:8888",
|
||||
"OS-EXT-IPS:type": "floating"
|
||||
}
|
||||
],
|
||||
"network2": [
|
||||
{
|
||||
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:02",
|
||||
"version": 4,
|
||||
"addr": "192.0.2.3",
|
||||
"OS-EXT-IPS:type": "floating"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expected: []serverSubnetAddresses{
|
||||
{
|
||||
NetworkName: "network1",
|
||||
Addresses: []serverAddress{
|
||||
{
|
||||
Type: fixedIP,
|
||||
IPVersion: ipV4,
|
||||
IP: "192.0.2.1",
|
||||
MAC: "00:00:00:00:00:00",
|
||||
},
|
||||
{
|
||||
Type: floatingIP,
|
||||
IPVersion: ipV6,
|
||||
IP: "2001:db8:3333:4444:5555:6666:7777:8888",
|
||||
MAC: "00:00:00:00:00:01",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
NetworkName: "network2",
|
||||
Addresses: []serverAddress{
|
||||
{
|
||||
Type: floatingIP,
|
||||
IPVersion: ipV4,
|
||||
IP: "192.0.2.3",
|
||||
MAC: "00:00:00:00:00:02",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid ip version": {
|
||||
input: `
|
||||
{
|
||||
"network1": [
|
||||
{
|
||||
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00",
|
||||
"version": 5,
|
||||
"addr": "192.0.2.1",
|
||||
"OS-EXT-IPS:type": "fixed"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid ip type": {
|
||||
input: `
|
||||
{
|
||||
"network1": [
|
||||
{
|
||||
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00",
|
||||
"version": 4,
|
||||
"addr": "192.0.2.1",
|
||||
"OS-EXT-IPS:type": "invalid"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid second lvl structure": {
|
||||
input: `
|
||||
{
|
||||
"network1": { "invalid": "structure" }
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid third lvl structure": {
|
||||
input: `
|
||||
{
|
||||
"network1": [ "invalid" ]
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
var addrsMap map[string]any
|
||||
err := json.Unmarshal([]byte(tc.input), &addrsMap)
|
||||
require.NoError(err)
|
||||
|
||||
addrs, err := parseSeverAddresses(addrsMap)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.ElementsMatch(tc.expected, addrs)
|
||||
})
|
||||
}
|
||||
}
|
26
internal/cloud/openstack/wrappers.go
Normal file
26
internal/cloud/openstack/wrappers.go
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"github.com/gophercloud/gophercloud"
|
||||
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
||||
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
|
||||
)
|
||||
|
||||
type apiClient struct {
|
||||
servers *gophercloud.ServiceClient
|
||||
subnets *gophercloud.ServiceClient
|
||||
}
|
||||
|
||||
func (c *apiClient) ListServers(opts servers.ListOptsBuilder) pagerAPI {
|
||||
return servers.List(c.servers, opts)
|
||||
}
|
||||
|
||||
func (c *apiClient) ListSubnets(opts subnets.ListOpts) pagerAPI {
|
||||
return subnets.List(c.subnets, opts)
|
||||
}
|
Loading…
Reference in New Issue
Block a user