mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-25 14:56:18 -05:00
openstack: implement imds and metadata self
Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
This commit is contained in:
parent
630016d1b3
commit
418f08bf40
23
internal/cloud/openstack/api.go
Normal file
23
internal/cloud/openstack/api.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||||
|
)
|
||||||
|
|
||||||
|
type imdsAPI interface {
|
||||||
|
providerID(ctx context.Context) (string, error)
|
||||||
|
name(ctx context.Context) (string, error)
|
||||||
|
projectID(ctx context.Context) (string, error)
|
||||||
|
uid(ctx context.Context) (string, error)
|
||||||
|
initSecretHash(ctx context.Context) (string, error)
|
||||||
|
role(ctx context.Context) (role.Role, error)
|
||||||
|
vpcIP(ctx context.Context) (string, error)
|
||||||
|
}
|
58
internal/cloud/openstack/api_test.go
Normal file
58
internal/cloud/openstack/api_test.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubIMDSClient struct {
|
||||||
|
providerIDResult string
|
||||||
|
providerIDErr error
|
||||||
|
nameResult string
|
||||||
|
nameErr error
|
||||||
|
projectIDResult string
|
||||||
|
projectIDErr error
|
||||||
|
uidResult string
|
||||||
|
uidErr error
|
||||||
|
initSecretHashResult string
|
||||||
|
initSecretHashErr error
|
||||||
|
roleResult role.Role
|
||||||
|
roleErr error
|
||||||
|
vpcIPResult string
|
||||||
|
vpcIPErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubIMDSClient) providerID(ctx context.Context) (string, error) {
|
||||||
|
return c.providerIDResult, c.providerIDErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubIMDSClient) name(ctx context.Context) (string, error) {
|
||||||
|
return c.nameResult, c.nameErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubIMDSClient) projectID(ctx context.Context) (string, error) {
|
||||||
|
return c.projectIDResult, c.projectIDErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubIMDSClient) uid(ctx context.Context) (string, error) {
|
||||||
|
return c.uidResult, c.uidErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubIMDSClient) initSecretHash(ctx context.Context) (string, error) {
|
||||||
|
return c.initSecretHashResult, c.initSecretHashErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubIMDSClient) role(ctx context.Context) (role.Role, error) {
|
||||||
|
return c.roleResult, c.roleErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubIMDSClient) vpcIP(ctx context.Context) (string, error) {
|
||||||
|
return c.vpcIPResult, c.vpcIPErr
|
||||||
|
}
|
259
internal/cloud/openstack/imds.go
Normal file
259
internal/cloud/openstack/imds.go
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||||
|
)
|
||||||
|
|
||||||
|
// documentation of OpenStack Metadata Service: https://docs.openstack.org/nova/rocky/user/metadata-service.html
|
||||||
|
|
||||||
|
const (
|
||||||
|
imdsMetaDataURL = "http://169.254.169.254/openstack/2018-08-27/meta_data.json"
|
||||||
|
ec2ImdsBaseURL = "http://169.254.169.254/1.0/meta-data"
|
||||||
|
maxCacheAge = 12 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
type imdsClient struct {
|
||||||
|
client httpClient
|
||||||
|
|
||||||
|
vpcIPCache string
|
||||||
|
vpcIPCacheTime time.Time
|
||||||
|
cache metadataResponse
|
||||||
|
cacheTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// providerID returns the provider ID of the instance the function is called from.
|
||||||
|
func (c *imdsClient) providerID(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || c.cache.UUID == "" {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cache.UUID == "" {
|
||||||
|
return "", errors.New("unable to get provider id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.UUID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imdsClient) name(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || c.cache.Name == "" {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cache.Name == "" {
|
||||||
|
return "", errors.New("unable to get name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// projectID returns the project ID of the instance the function
|
||||||
|
// is called from.
|
||||||
|
func (c *imdsClient) projectID(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || c.cache.ProjectID == "" {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cache.ProjectID == "" {
|
||||||
|
return "", errors.New("unable to get project id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.ProjectID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// uid returns the UID of the cluster, based on the tags on the instance
|
||||||
|
// the function is called from, which are inherited from the scale set.
|
||||||
|
func (c *imdsClient) uid(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || len(c.cache.Tags.UID) == 0 {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.cache.Tags.UID) == 0 {
|
||||||
|
return "", fmt.Errorf("unable to get uid from metadata tags %v", c.cache.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.Tags.UID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSecretHash returns the hash of the init secret of the cluster, based on the tags on the instance
|
||||||
|
// the function is called from, which are inherited from the scale set.
|
||||||
|
func (c *imdsClient) initSecretHash(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || len(c.cache.Tags.InitSecretHash) == 0 {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.cache.Tags.InitSecretHash) == 0 {
|
||||||
|
return "", fmt.Errorf("unable to get tag %s from metadata tags %v", cloud.TagInitSecretHash, c.cache.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.Tags.InitSecretHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// role returns the role of the instance the function is called from.
|
||||||
|
func (c *imdsClient) role(ctx context.Context) (role.Role, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || len(c.cache.Tags.Role) == 0 {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return role.Unknown, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.cache.Tags.Role) == 0 {
|
||||||
|
return role.Unknown, fmt.Errorf("unable to get role from metadata tags %v", c.cache.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return role.FromString(c.cache.Tags.Role), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imdsClient) authURL(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || c.cache.Tags.AuthURL == "" {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cache.Tags.AuthURL == "" {
|
||||||
|
return "", errors.New("unable to get auth url")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.Tags.AuthURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imdsClient) userDomainName(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || c.cache.Tags.UserDomainName == "" {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cache.Tags.UserDomainName == "" {
|
||||||
|
return "", errors.New("unable to get user domain name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.Tags.UserDomainName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imdsClient) username(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || c.cache.Tags.Username == "" {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cache.Tags.Username == "" {
|
||||||
|
return "", errors.New("unable to get token name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.Tags.Username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imdsClient) password(ctx context.Context) (string, error) {
|
||||||
|
if c.timeForUpdate(c.cacheTime) || c.cache.Tags.Password == "" {
|
||||||
|
if err := c.update(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cache.Tags.Password == "" {
|
||||||
|
return "", errors.New("unable to get token password")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache.Tags.Password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeForUpdate checks whether an update is needed due to cache age.
|
||||||
|
func (c *imdsClient) timeForUpdate(t time.Time) bool {
|
||||||
|
return time.Since(t) > maxCacheAge
|
||||||
|
}
|
||||||
|
|
||||||
|
// update updates instance metadata from the azure imds API.
|
||||||
|
func (c *imdsClient) update(ctx context.Context) error {
|
||||||
|
resp, err := httpGet(ctx, c.client, imdsMetaDataURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var metadataResp metadataResponse
|
||||||
|
if err := json.Unmarshal(resp, &metadataResp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.cache = metadataResp
|
||||||
|
c.cacheTime = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imdsClient) vpcIP(ctx context.Context) (string, error) {
|
||||||
|
const path = "local-ipv4"
|
||||||
|
|
||||||
|
if c.timeForUpdate(c.vpcIPCacheTime) || c.vpcIPCache == "" {
|
||||||
|
resp, err := httpGet(ctx, c.client, ec2ImdsBaseURL+"/"+path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
c.vpcIPCache = string(resp)
|
||||||
|
c.vpcIPCacheTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.vpcIPCache == "" {
|
||||||
|
return "", errors.New("unable to get vpc ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.vpcIPCache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpGet(ctx context.Context, c httpClient, url string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// metadataResponse contains metadataResponse with only the required values.
|
||||||
|
type metadataResponse struct {
|
||||||
|
UUID string `json:"uuid,omitempty"`
|
||||||
|
ProjectID string `json:"project_id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Tags metadataTags `json:"meta,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type metadataTags struct {
|
||||||
|
InitSecretHash string `json:"constellation-init-secret-hash,omitempty"`
|
||||||
|
Role string `json:"constellation-role,omitempty"`
|
||||||
|
UID string `json:"constellation-uid,omitempty"`
|
||||||
|
AuthURL string `json:"openstack-auth-url,omitempty"`
|
||||||
|
UserDomainName string `json:"openstack-user-domain-name,omitempty"`
|
||||||
|
Username string `json:"openstack-username,omitempty"`
|
||||||
|
Password string `json:"openstack-password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpClient interface {
|
||||||
|
Do(req *http.Request) (*http.Response, error)
|
||||||
|
}
|
402
internal/cloud/openstack/imds_test.go
Normal file
402
internal/cloud/openstack/imds_test.go
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProviderID(t *testing.T) {
|
||||||
|
someErr := errors.New("failed")
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
cache metadataResponse
|
||||||
|
cacheTime time.Time
|
||||||
|
newClient httpClientJSONCreateFunc
|
||||||
|
wantResult string
|
||||||
|
wantCall bool
|
||||||
|
wantErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
newTestCases := func(mResp1, mResp2 metadataResponse, expect1, expect2 string) map[string]testCase {
|
||||||
|
return map[string]testCase{
|
||||||
|
"cached": {
|
||||||
|
cache: mResp1,
|
||||||
|
cacheTime: time.Now(),
|
||||||
|
wantResult: expect1,
|
||||||
|
wantCall: false,
|
||||||
|
},
|
||||||
|
"from http": {
|
||||||
|
newClient: newStubHTTPClientJSONFunc(mResp1, nil),
|
||||||
|
wantResult: expect1,
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"cache outdated": {
|
||||||
|
cache: mResp1,
|
||||||
|
cacheTime: time.Now().AddDate(0, 0, -1),
|
||||||
|
newClient: newStubHTTPClientJSONFunc(mResp2, nil),
|
||||||
|
wantResult: expect2,
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"cache empty": {
|
||||||
|
cacheTime: time.Now(),
|
||||||
|
newClient: newStubHTTPClientJSONFunc(mResp1, nil),
|
||||||
|
wantResult: expect1,
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"http error": {
|
||||||
|
newClient: newStubHTTPClientJSONFunc(metadataResponse{}, someErr),
|
||||||
|
wantCall: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"http empty response": {
|
||||||
|
newClient: newStubHTTPClientJSONFunc(metadataResponse{}, nil),
|
||||||
|
wantCall: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testUnits := map[string]struct {
|
||||||
|
method func(c *imdsClient, ctx context.Context) (string, error)
|
||||||
|
testCases map[string]testCase
|
||||||
|
}{
|
||||||
|
"providerID": {
|
||||||
|
method: (*imdsClient).providerID,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{UUID: "uuid1"},
|
||||||
|
metadataResponse{UUID: "uuid2"},
|
||||||
|
"uuid1", "uuid2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
method: (*imdsClient).name,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{Name: "name1"},
|
||||||
|
metadataResponse{Name: "name2"},
|
||||||
|
"name1", "name2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"projectID": {
|
||||||
|
method: (*imdsClient).projectID,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{ProjectID: "projectID1"},
|
||||||
|
metadataResponse{ProjectID: "projectID2"},
|
||||||
|
"projectID1", "projectID2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
method: (*imdsClient).uid,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{Tags: metadataTags{UID: "uid1"}},
|
||||||
|
metadataResponse{Tags: metadataTags{UID: "uid2"}},
|
||||||
|
"uid1", "uid2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"initSecretHash": {
|
||||||
|
method: (*imdsClient).initSecretHash,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{Tags: metadataTags{InitSecretHash: "hash1"}},
|
||||||
|
metadataResponse{Tags: metadataTags{InitSecretHash: "hash2"}},
|
||||||
|
"hash1", "hash2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"authURL": {
|
||||||
|
method: (*imdsClient).authURL,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{Tags: metadataTags{AuthURL: "authURL1"}},
|
||||||
|
metadataResponse{Tags: metadataTags{AuthURL: "authURL2"}},
|
||||||
|
"authURL1", "authURL2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"userDomainName": {
|
||||||
|
method: (*imdsClient).userDomainName,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{Tags: metadataTags{UserDomainName: "userDomainName1"}},
|
||||||
|
metadataResponse{Tags: metadataTags{UserDomainName: "userDomainName2"}},
|
||||||
|
"userDomainName1", "userDomainName2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
method: (*imdsClient).username,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{Tags: metadataTags{Username: "username1"}},
|
||||||
|
metadataResponse{Tags: metadataTags{Username: "username2"}},
|
||||||
|
"username1", "username2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
method: (*imdsClient).password,
|
||||||
|
testCases: newTestCases(
|
||||||
|
metadataResponse{Tags: metadataTags{Password: "password1"}},
|
||||||
|
metadataResponse{Tags: metadataTags{Password: "password2"}},
|
||||||
|
"password1", "password2",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tu := range testUnits {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
for name, tc := range tu.testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
var client *stubHTTPClientJSON
|
||||||
|
if tc.newClient != nil {
|
||||||
|
client = tc.newClient(require)
|
||||||
|
}
|
||||||
|
imds := &imdsClient{
|
||||||
|
client: client,
|
||||||
|
cache: tc.cache,
|
||||||
|
cacheTime: tc.cacheTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := tu.method(imds, context.Background())
|
||||||
|
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(tc.wantResult, result)
|
||||||
|
if client != nil {
|
||||||
|
assert.Equal(tc.wantCall, client.called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRole(t *testing.T) {
|
||||||
|
someErr := errors.New("failed")
|
||||||
|
mResp1 := metadataResponse{Tags: metadataTags{Role: "control-plane"}}
|
||||||
|
mResp2 := metadataResponse{Tags: metadataTags{Role: "worker"}}
|
||||||
|
expect1 := role.ControlPlane
|
||||||
|
expect2 := role.Worker
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
cache metadataResponse
|
||||||
|
cacheTime time.Time
|
||||||
|
newClient httpClientJSONCreateFunc
|
||||||
|
wantResult role.Role
|
||||||
|
wantCall bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"cached": {
|
||||||
|
cache: mResp1,
|
||||||
|
cacheTime: time.Now(),
|
||||||
|
wantResult: expect1,
|
||||||
|
wantCall: false,
|
||||||
|
},
|
||||||
|
"from http": {
|
||||||
|
newClient: newStubHTTPClientJSONFunc(mResp1, nil),
|
||||||
|
wantResult: expect1,
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"cache outdated": {
|
||||||
|
cache: mResp1,
|
||||||
|
cacheTime: time.Now().AddDate(0, 0, -1),
|
||||||
|
newClient: newStubHTTPClientJSONFunc(mResp2, nil),
|
||||||
|
wantResult: expect2,
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"cache empty": {
|
||||||
|
cacheTime: time.Now(),
|
||||||
|
newClient: newStubHTTPClientJSONFunc(mResp1, nil),
|
||||||
|
wantResult: expect1,
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"http error": {
|
||||||
|
newClient: newStubHTTPClientJSONFunc(metadataResponse{}, someErr),
|
||||||
|
wantCall: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"http empty response": {
|
||||||
|
newClient: newStubHTTPClientJSONFunc(metadataResponse{}, nil),
|
||||||
|
wantCall: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
var client *stubHTTPClientJSON
|
||||||
|
if tc.newClient != nil {
|
||||||
|
client = tc.newClient(require)
|
||||||
|
}
|
||||||
|
imds := &imdsClient{
|
||||||
|
client: client,
|
||||||
|
cache: tc.cache,
|
||||||
|
cacheTime: tc.cacheTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := imds.role(context.Background())
|
||||||
|
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(tc.wantResult, result)
|
||||||
|
if client != nil {
|
||||||
|
assert.Equal(tc.wantCall, client.called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPCIP(t *testing.T) {
|
||||||
|
someErr := errors.New("failed")
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
cache string
|
||||||
|
cacheTime time.Time
|
||||||
|
client *stubHTTPClient
|
||||||
|
wantResult string
|
||||||
|
wantCall bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"cached": {
|
||||||
|
cache: "192.0.2.1",
|
||||||
|
cacheTime: time.Now(),
|
||||||
|
wantResult: "192.0.2.1",
|
||||||
|
wantCall: false,
|
||||||
|
},
|
||||||
|
"from http": {
|
||||||
|
client: &stubHTTPClient{response: "192.0.2.1"},
|
||||||
|
wantResult: "192.0.2.1",
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"cache outdated": {
|
||||||
|
cache: "192.0.2.1",
|
||||||
|
cacheTime: time.Now().AddDate(0, 0, -1),
|
||||||
|
client: &stubHTTPClient{response: "192.0.2.2"},
|
||||||
|
wantResult: "192.0.2.2",
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"cache empty": {
|
||||||
|
cacheTime: time.Now(),
|
||||||
|
client: &stubHTTPClient{response: "192.0.2.1"},
|
||||||
|
wantResult: "192.0.2.1",
|
||||||
|
wantCall: true,
|
||||||
|
},
|
||||||
|
"http error": {
|
||||||
|
client: &stubHTTPClient{err: someErr},
|
||||||
|
wantCall: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"http empty response": {
|
||||||
|
client: &stubHTTPClient{},
|
||||||
|
wantCall: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
imds := &imdsClient{
|
||||||
|
client: tc.client,
|
||||||
|
vpcIPCache: tc.cache,
|
||||||
|
vpcIPCacheTime: tc.cacheTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := imds.vpcIP(context.Background())
|
||||||
|
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(tc.wantResult, result)
|
||||||
|
if tc.client != nil {
|
||||||
|
assert.Equal(tc.wantCall, tc.client.called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeForUpdate(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
cacheTime time.Time
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
"cache outdated": {
|
||||||
|
cacheTime: time.Now().AddDate(-1, 0, -1),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
"cache not outdated": {
|
||||||
|
cacheTime: time.Now(),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
imds := &imdsClient{cacheTime: tc.cacheTime}
|
||||||
|
|
||||||
|
assert.Equal(tc.want, imds.timeForUpdate(tc.cacheTime))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpClientJSONCreateFunc func(r *require.Assertions) *stubHTTPClientJSON
|
||||||
|
|
||||||
|
type stubHTTPClientJSON struct {
|
||||||
|
require *require.Assertions
|
||||||
|
response metadataResponse
|
||||||
|
err error
|
||||||
|
called bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStubHTTPClientJSONFunc(response metadataResponse, err error) httpClientJSONCreateFunc {
|
||||||
|
return func(r *require.Assertions) *stubHTTPClientJSON {
|
||||||
|
return &stubHTTPClientJSON{
|
||||||
|
response: response,
|
||||||
|
err: err,
|
||||||
|
require: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubHTTPClientJSON) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
c.called = true
|
||||||
|
body, err := json.Marshal(c.response)
|
||||||
|
c.require.NoError(err)
|
||||||
|
return &http.Response{Body: io.NopCloser(bytes.NewReader(body))}, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubHTTPClient struct {
|
||||||
|
response string
|
||||||
|
err error
|
||||||
|
called bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *stubHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
c.called = true
|
||||||
|
return &http.Response{Body: io.NopCloser(strings.NewReader(c.response))}, c.err
|
||||||
|
}
|
53
internal/cloud/openstack/openstack.go
Normal file
53
internal/cloud/openstack/openstack.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cloud is the metadata client for OpenStack.
|
||||||
|
type Cloud struct {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self returns the metadata of the current instance.
|
||||||
|
func (c *Cloud) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
||||||
|
name, err := c.imds.name(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return metadata.InstanceMetadata{}, fmt.Errorf("getting name: %w", err)
|
||||||
|
}
|
||||||
|
providerID, err := c.imds.providerID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return metadata.InstanceMetadata{}, fmt.Errorf("getting provider id: %w", err)
|
||||||
|
}
|
||||||
|
role, err := c.imds.role(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return metadata.InstanceMetadata{}, fmt.Errorf("getting role: %w", err)
|
||||||
|
}
|
||||||
|
vpcIP, err := c.imds.vpcIP(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return metadata.InstanceMetadata{}, fmt.Errorf("getting vpc ip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.InstanceMetadata{
|
||||||
|
Name: name,
|
||||||
|
ProviderID: providerID,
|
||||||
|
Role: role,
|
||||||
|
VPCIP: vpcIP,
|
||||||
|
}, nil
|
||||||
|
}
|
95
internal/cloud/openstack/openstack_test.go
Normal file
95
internal/cloud/openstack/openstack_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user