diff --git a/internal/cloud/openstack/imds.go b/internal/cloud/openstack/imds.go index e977558d9..792a0d881 100644 --- a/internal/cloud/openstack/imds.go +++ b/internal/cloud/openstack/imds.go @@ -23,6 +23,7 @@ import ( const ( imdsMetaDataURL = "http://169.254.169.254/openstack/2018-08-27/meta_data.json" + imdsUserDataURL = "http://169.254.169.254/openstack/2018-08-27/user_data" ec2ImdsBaseURL = "http://169.254.169.254/1.0/meta-data" maxCacheAge = 12 * time.Hour ) @@ -33,6 +34,7 @@ type imdsClient struct { vpcIPCache string vpcIPCacheTime time.Time cache metadataResponse + userDataCache userDataResponse cacheTime time.Time } @@ -129,73 +131,73 @@ func (c *imdsClient) role(ctx context.Context) (role.Role, error) { } func (c *imdsClient) loadBalancerEndpoint(ctx context.Context) (string, error) { - if c.timeForUpdate(c.cacheTime) || c.cache.Tags.LoadBalancerEndpoint == "" { + if c.timeForUpdate(c.cacheTime) || c.userDataCache.LoadBalancerEndpoint == "" { if err := c.update(ctx); err != nil { return "", err } } - if c.cache.Tags.LoadBalancerEndpoint == "" { + if c.userDataCache.LoadBalancerEndpoint == "" { return "", errors.New("unable to get load balancer endpoint") } - return c.cache.Tags.LoadBalancerEndpoint, nil + return c.userDataCache.LoadBalancerEndpoint, nil } func (c *imdsClient) authURL(ctx context.Context) (string, error) { - if c.timeForUpdate(c.cacheTime) || c.cache.Tags.AuthURL == "" { + if c.timeForUpdate(c.cacheTime) || c.userDataCache.AuthURL == "" { if err := c.update(ctx); err != nil { return "", err } } - if c.cache.Tags.AuthURL == "" { + if c.userDataCache.AuthURL == "" { return "", errors.New("unable to get auth url") } - return c.cache.Tags.AuthURL, nil + return c.userDataCache.AuthURL, nil } func (c *imdsClient) userDomainName(ctx context.Context) (string, error) { - if c.timeForUpdate(c.cacheTime) || c.cache.Tags.UserDomainName == "" { + if c.timeForUpdate(c.cacheTime) || c.userDataCache.UserDomainName == "" { if err := c.update(ctx); err != nil { return "", err } } - if c.cache.Tags.UserDomainName == "" { + if c.userDataCache.UserDomainName == "" { return "", errors.New("unable to get user domain name") } - return c.cache.Tags.UserDomainName, nil + return c.userDataCache.UserDomainName, nil } func (c *imdsClient) username(ctx context.Context) (string, error) { - if c.timeForUpdate(c.cacheTime) || c.cache.Tags.Username == "" { + if c.timeForUpdate(c.cacheTime) || c.userDataCache.Username == "" { if err := c.update(ctx); err != nil { return "", err } } - if c.cache.Tags.Username == "" { + if c.userDataCache.Username == "" { return "", errors.New("unable to get token name") } - return c.cache.Tags.Username, nil + return c.userDataCache.Username, nil } func (c *imdsClient) password(ctx context.Context) (string, error) { - if c.timeForUpdate(c.cacheTime) || c.cache.Tags.Password == "" { + if c.timeForUpdate(c.cacheTime) || c.userDataCache.Password == "" { if err := c.update(ctx); err != nil { return "", err } } - if c.cache.Tags.Password == "" { + if c.userDataCache.Password == "" { return "", errors.New("unable to get token password") } - return c.cache.Tags.Password, nil + return c.userDataCache.Password, nil } // timeForUpdate checks whether an update is needed due to cache age. @@ -203,18 +205,41 @@ 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 { + if err := c.updateInstanceMetadata(ctx); err != nil { + return fmt.Errorf("updating instance metadata: %w", err) + } + if err := c.updateUserData(ctx); err != nil { + return fmt.Errorf("updating user data: %w", err) + } + c.cacheTime = time.Now() + return nil +} + +// update updates instance metadata from the azure imds API. +func (c *imdsClient) updateInstanceMetadata(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 fmt.Errorf("unmarshalling IMDS metadata response %q: %w", string(resp), err) + return fmt.Errorf("unmarshalling IMDS metadata response %q: %w", resp, err) } c.cache = metadataResp - c.cacheTime = time.Now() + return nil +} + +func (c *imdsClient) updateUserData(ctx context.Context) error { + resp, err := httpGet(ctx, c.client, imdsUserDataURL) + if err != nil { + return err + } + var userdataResp userDataResponse + if err := json.Unmarshal(resp, &userdataResp); err != nil { + return fmt.Errorf("unmarshalling IMDS user_data response %q: %w", resp, err) + } + c.userDataCache = userdataResp return nil } @@ -262,9 +287,12 @@ type metadataResponse struct { } type metadataTags struct { - InitSecretHash string `json:"constellation-init-secret-hash,omitempty"` - Role string `json:"constellation-role,omitempty"` - UID string `json:"constellation-uid,omitempty"` + InitSecretHash string `json:"constellation-init-secret-hash,omitempty"` + Role string `json:"constellation-role,omitempty"` + UID string `json:"constellation-uid,omitempty"` +} + +type userDataResponse struct { AuthURL string `json:"openstack-auth-url,omitempty"` UserDomainName string `json:"openstack-user-domain-name,omitempty"` Username string `json:"openstack-username,omitempty"` diff --git a/internal/cloud/openstack/imds_test.go b/internal/cloud/openstack/imds_test.go index 94caaa108..ce45dbd3d 100644 --- a/internal/cloud/openstack/imds_test.go +++ b/internal/cloud/openstack/imds_test.go @@ -26,7 +26,7 @@ func TestProviderID(t *testing.T) { someErr := errors.New("failed") type testCase struct { - cache metadataResponse + cache any cacheTime time.Time newClient httpClientJSONCreateFunc wantResult string @@ -34,7 +34,7 @@ func TestProviderID(t *testing.T) { wantErr bool } - newTestCases := func(mResp1, mResp2 metadataResponse, expect1, expect2 string) map[string]testCase { + newTestCases := func(mResp1, mResp2 any, expect1, expect2 string) map[string]testCase { return map[string]testCase{ "cached": { cache: mResp1, @@ -120,32 +120,32 @@ func TestProviderID(t *testing.T) { "authURL": { method: (*imdsClient).authURL, testCases: newTestCases( - metadataResponse{Tags: metadataTags{AuthURL: "authURL1"}}, - metadataResponse{Tags: metadataTags{AuthURL: "authURL2"}}, + userDataResponse{AuthURL: "authURL1"}, + userDataResponse{AuthURL: "authURL2"}, "authURL1", "authURL2", ), }, "userDomainName": { method: (*imdsClient).userDomainName, testCases: newTestCases( - metadataResponse{Tags: metadataTags{UserDomainName: "userDomainName1"}}, - metadataResponse{Tags: metadataTags{UserDomainName: "userDomainName2"}}, + userDataResponse{UserDomainName: "userDomainName1"}, + userDataResponse{UserDomainName: "userDomainName2"}, "userDomainName1", "userDomainName2", ), }, "username": { method: (*imdsClient).username, testCases: newTestCases( - metadataResponse{Tags: metadataTags{Username: "username1"}}, - metadataResponse{Tags: metadataTags{Username: "username2"}}, + userDataResponse{Username: "username1"}, + userDataResponse{Username: "username2"}, "username1", "username2", ), }, "password": { method: (*imdsClient).password, testCases: newTestCases( - metadataResponse{Tags: metadataTags{Password: "password1"}}, - metadataResponse{Tags: metadataTags{Password: "password2"}}, + userDataResponse{Password: "password1"}, + userDataResponse{Password: "password2"}, "password1", "password2", ), }, @@ -162,10 +162,18 @@ func TestProviderID(t *testing.T) { if tc.newClient != nil { client = tc.newClient(require) } + var cache metadataResponse + var userDataCache userDataResponse + if _, ok := tc.cache.(metadataResponse); ok { + cache = tc.cache.(metadataResponse) + } else if _, ok := tc.cache.(userDataResponse); ok { + userDataCache = tc.cache.(userDataResponse) + } imds := &imdsClient{ - client: client, - cache: tc.cache, - cacheTime: tc.cacheTime, + client: client, + cache: cache, + userDataCache: userDataCache, + cacheTime: tc.cacheTime, } result, err := tu.method(imds, context.Background()) @@ -373,13 +381,13 @@ type httpClientJSONCreateFunc func(r *require.Assertions) *stubHTTPClientJSON type stubHTTPClientJSON struct { require *require.Assertions - response metadataResponse + response any code int err error called bool } -func newStubHTTPClientJSONFunc(response metadataResponse, statusCode int, err error) httpClientJSONCreateFunc { +func newStubHTTPClientJSONFunc(response any, statusCode int, err error) httpClientJSONCreateFunc { return func(r *require.Assertions) *stubHTTPClientJSON { return &stubHTTPClientJSON{ response: response, diff --git a/terraform/infrastructure/openstack/modules/instance_group/main.tf b/terraform/infrastructure/openstack/modules/instance_group/main.tf index 0d2cdc77a..0a10b1531 100644 --- a/terraform/infrastructure/openstack/modules/instance_group/main.tf +++ b/terraform/infrastructure/openstack/modules/instance_group/main.tf @@ -72,14 +72,16 @@ resource "openstack_compute_instance_v2" "instance_group_member" { delete_on_termination = true } metadata = { - constellation-role = var.role - constellation-uid = var.uid - constellation-init-secret-hash = var.init_secret_hash + constellation-role = var.role + constellation-uid = var.uid + constellation-init-secret-hash = var.init_secret_hash + } + user_data = jsonencode({ openstack-auth-url = var.identity_internal_url openstack-username = var.openstack_username openstack-password = var.openstack_password openstack-user-domain-name = var.openstack_user_domain_name openstack-load-balancer-endpoint = var.openstack_load_balancer_endpoint - } - availability_zone_hints = var.availability_zone + }) + availability_zone_hints = length(var.availability_zone) > 0 ? var.availability_zone : null }