constellation/internal/cloud/azure/imds.go
Daniel Weiße f41c54e837
AB#2524 Refactor Azure metadata/cloud API (#477)
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
2022-11-15 09:08:18 +01:00

188 lines
4.9 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package azure
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/edgelesssys/constellation/v2/internal/cloud"
"github.com/edgelesssys/constellation/v2/internal/role"
)
// subset of azure imds API: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux
// this is not yet available through the azure sdk (see https://github.com/Azure/azure-rest-api-specs/issues/4408)
const (
imdsURL = "http://169.254.169.254/metadata/instance"
imdsAPIVersion = "2021-02-01"
maxCacheAge = 12 * time.Hour
)
type imdsClient struct {
client *http.Client
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.cache.Compute.ResourceID == "" {
if err := c.update(ctx); err != nil {
return "", err
}
}
if c.cache.Compute.ResourceID == "" {
return "", errors.New("unable to get provider id")
}
return c.cache.Compute.ResourceID, nil
}
func (c *imdsClient) name(ctx context.Context) (string, error) {
if c.timeForUpdate() || c.cache.Compute.OSProfile.ComputerName == "" {
if err := c.update(ctx); err != nil {
return "", err
}
}
if c.cache.Compute.OSProfile.ComputerName == "" {
return "", errors.New("unable to get name")
}
return c.cache.Compute.OSProfile.ComputerName, nil
}
// subscriptionID returns the subscription ID of the instance the function
// is called from.
func (c *imdsClient) subscriptionID(ctx context.Context) (string, error) {
if c.timeForUpdate() || c.cache.Compute.SubscriptionID == "" {
if err := c.update(ctx); err != nil {
return "", err
}
}
if c.cache.Compute.SubscriptionID == "" {
return "", errors.New("unable to get subscription id")
}
return c.cache.Compute.SubscriptionID, nil
}
// resourceGroup returns the resource group of the instance the function
// is called from.
func (c *imdsClient) resourceGroup(ctx context.Context) (string, error) {
if c.timeForUpdate() || c.cache.Compute.ResourceGroup == "" {
if err := c.update(ctx); err != nil {
return "", err
}
}
if c.cache.Compute.ResourceGroup == "" {
return "", errors.New("unable to get resource group")
}
return c.cache.Compute.ResourceGroup, 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() || len(c.cache.Compute.Tags) == 0 {
if err := c.update(ctx); err != nil {
return "", err
}
}
for _, tag := range c.cache.Compute.Tags {
if tag.Name == cloud.TagUID {
return tag.Value, nil
}
}
return "", fmt.Errorf("unable to get uid from metadata tags %v", c.cache.Compute.Tags)
}
// 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() || len(c.cache.Compute.Tags) == 0 {
if err := c.update(ctx); err != nil {
return role.Unknown, err
}
}
for _, tag := range c.cache.Compute.Tags {
if tag.Name == cloud.TagRole {
return role.FromString(tag.Value), nil
}
}
return role.Unknown, fmt.Errorf("unable to get role from metadata tags %v", c.cache.Compute.Tags)
}
// timeForUpdate checks whether an update is needed due to cache age.
func (c *imdsClient) timeForUpdate() bool {
return time.Since(c.cacheTime) > maxCacheAge
}
// update updates instance metadata from the azure imds API.
func (c *imdsClient) update(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imdsURL, http.NoBody)
if err != nil {
return err
}
req.Header.Add("Metadata", "True")
query := req.URL.Query()
query.Add("format", "json")
query.Add("api-version", imdsAPIVersion)
req.URL.RawQuery = query.Encode()
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var res metadataResponse
if err := json.Unmarshal(body, &res); err != nil {
return err
}
c.cache = res
c.cacheTime = time.Now()
return nil
}
// metadataResponse contains metadataResponse with only the required values.
type metadataResponse struct {
Compute metadataResponseCompute `json:"compute,omitempty"`
}
type metadataResponseCompute struct {
ResourceID string `json:"resourceId,omitempty"`
SubscriptionID string `json:"subscriptionId,omitempty"`
ResourceGroup string `json:"resourceGroupName,omitempty"`
Tags []metadataTag `json:"tagsList,omitempty"`
OSProfile struct {
ComputerName string `json:"computerName,omitempty"`
} `json:"osProfile,omitempty"`
}
type metadataTag struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
}