mirror of
				https://github.com/edgelesssys/constellation.git
				synced 2025-10-31 03:39:04 -04: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
					
				
					 6 changed files with 890 additions and 0 deletions
				
			
		
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Paul Meyer
						Paul Meyer