Преглед на файлове

Merge branch 'develop' into chore/clean-logging-in-metrics-go

Bernard Grymonpon преди 2 години
родител
ревизия
9310a17ccf

+ 20 - 40
pkg/cloud/azure/authorizer.go

@@ -1,80 +1,60 @@
 package azure
 
 import (
-	"encoding/json"
 	"fmt"
 
-	"github.com/Azure/azure-storage-blob-go/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
+	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
 	"github.com/opencost/opencost/pkg/cloud"
+	"github.com/opencost/opencost/pkg/util/json"
 )
 
-const AccessKeyAuthorizerType = "AzureAccessKey"
+const DefaultCredentialAuthorizerType = "AzureDefaultCredential"
 
+// Authorizer configs provide credentials from azidentity to connect to Azure services.
 type Authorizer interface {
 	cloud.Authorizer
-	GetBlobCredentials() (azblob.Credential, error)
+	GetCredential() (azcore.TokenCredential, error)
 }
 
 // SelectAuthorizerByType is an implementation of AuthorizerSelectorFn and acts as a register for Authorizer types
 func SelectAuthorizerByType(typeStr string) (Authorizer, error) {
 	switch typeStr {
-	case AccessKeyAuthorizerType:
-		return &AccessKey{}, nil
+	case DefaultCredentialAuthorizerType:
+		return &DefaultAzureCredentialHolder{}, nil
 	default:
 		return nil, fmt.Errorf("azure: provider authorizer type '%s' is not valid", typeStr)
 	}
 }
 
-type AccessKey struct {
-	AccessKey string `json:"accessKey"`
-	Account   string `json:"account"`
-}
+type DefaultAzureCredentialHolder struct{}
+
+func (dac *DefaultAzureCredentialHolder) MarshalJSON() ([]byte, error) {
+	fmap := make(map[string]any, 1)
+	fmap[cloud.AuthorizerTypeProperty] = DefaultCredentialAuthorizerType
 
-func (ak *AccessKey) MarshalJSON() ([]byte, error) {
-	fmap := make(map[string]any, 3)
-	fmap[cloud.AuthorizerTypeProperty] = AccessKeyAuthorizerType
-	fmap["accessKey"] = ak.AccessKey
-	fmap["account"] = ak.Account
 	return json.Marshal(fmap)
 }
 
-func (ak *AccessKey) Validate() error {
-	if ak.AccessKey == "" {
-		return fmt.Errorf("AccessKey: missing access key")
-	}
-	if ak.Account == "" {
-		return fmt.Errorf("AccessKey: missing account")
-	}
+func (dac *DefaultAzureCredentialHolder) Validate() error {
 	return nil
 }
 
-func (ak *AccessKey) Equals(config cloud.Config) bool {
+func (dac *DefaultAzureCredentialHolder) Equals(config cloud.Config) bool {
 	if config == nil {
 		return false
 	}
-	thatConfig, ok := config.(*AccessKey)
+	_, ok := config.(*DefaultAzureCredentialHolder)
 	if !ok {
 		return false
 	}
-
-	if ak.AccessKey != thatConfig.AccessKey {
-		return false
-	}
-	if ak.Account != thatConfig.Account {
-		return false
-	}
-
 	return true
 }
 
-func (ak *AccessKey) Sanitize() cloud.Config {
-	return &AccessKey{
-		AccessKey: cloud.Redacted,
-		Account:   ak.Account,
-	}
+func (dac *DefaultAzureCredentialHolder) Sanitize() cloud.Config {
+	return &DefaultAzureCredentialHolder{}
 }
 
-func (ak *AccessKey) GetBlobCredentials() (azblob.Credential, error) {
-	// Create a default request pipeline using your storage account name and account key.
-	return azblob.NewSharedKeyCredential(ak.Account, ak.AccessKey)
+func (dac *DefaultAzureCredentialHolder) GetCredential() (azcore.TokenCredential, error) {
+	return azidentity.NewDefaultAzureCredential(nil)
 }

+ 124 - 0
pkg/cloud/azure/storageauthorizer.go

@@ -0,0 +1,124 @@
+package azure
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/opencost/opencost/pkg/cloud"
+)
+
+const SharedKeyAuthorizerType = "AzureAccessKey"
+
+// StorageAuthorizer is a service specific Authorizer for Azure Storage, it exists so that we can support existing Shared
+// Key configurations while allowing the Authorizer to have a service agnostic api
+type StorageAuthorizer interface {
+	cloud.Authorizer
+	GetBlobClient(serviceURL string) (*azblob.Client, error)
+}
+
+// SelectStorageAuthorizerByType is an implementation of AuthorizerSelectorFn and acts as a register for Authorizer types
+func SelectStorageAuthorizerByType(typeStr string) (StorageAuthorizer, error) {
+	switch typeStr {
+	case SharedKeyAuthorizerType:
+		return &SharedKeyCredential{}, nil
+	default:
+		authorizer, err := SelectAuthorizerByType(typeStr)
+		if err != nil {
+			return nil, err
+		}
+		return &AuthorizerHolder{authorizer}, nil
+	}
+}
+
+// SharedKeyCredential is a StorageAuthorizer with credentials which cannot be used to authorize other services. This
+// is a legacy auth method which is not included in azidentity
+type SharedKeyCredential struct {
+	AccessKey string `json:"accessKey"`
+	Account   string `json:"account"`
+}
+
+func (skc *SharedKeyCredential) MarshalJSON() ([]byte, error) {
+	fmap := make(map[string]any, 3)
+	fmap[cloud.AuthorizerTypeProperty] = SharedKeyAuthorizerType
+	fmap["accessKey"] = skc.AccessKey
+	fmap["account"] = skc.Account
+	return json.Marshal(fmap)
+}
+
+func (skc *SharedKeyCredential) Validate() error {
+	if skc.AccessKey == "" {
+		return fmt.Errorf("SharedKeyCredential: missing access key")
+	}
+	if skc.Account == "" {
+		return fmt.Errorf("SharedKeyCredential: missing account")
+	}
+	return nil
+}
+
+func (skc *SharedKeyCredential) Equals(config cloud.Config) bool {
+	if config == nil {
+		return false
+	}
+	thatConfig, ok := config.(*SharedKeyCredential)
+	if !ok {
+		return false
+	}
+
+	if skc.AccessKey != thatConfig.AccessKey {
+		return false
+	}
+	if skc.Account != thatConfig.Account {
+		return false
+	}
+
+	return true
+}
+
+func (skc *SharedKeyCredential) Sanitize() cloud.Config {
+	return &SharedKeyCredential{
+		AccessKey: cloud.Redacted,
+		Account:   skc.Account,
+	}
+}
+
+func (skc *SharedKeyCredential) GetBlobClient(serviceURL string) (*azblob.Client, error) {
+	credential, err := azblob.NewSharedKeyCredential(skc.Account, skc.AccessKey)
+	if err != nil {
+		return nil, err
+	}
+	client, err := azblob.NewClientWithSharedKeyCredential(serviceURL, credential, nil)
+	return client, err
+}
+
+// AuthorizerHolder is a StorageAuthorizer implementation that wraps an Authorizer implementation
+type AuthorizerHolder struct {
+	Authorizer
+}
+
+func (ah *AuthorizerHolder) Equals(config cloud.Config) bool {
+	if config == nil {
+		return false
+	}
+	that, ok := config.(*AuthorizerHolder)
+	if !ok {
+		return false
+	}
+
+	return ah.Authorizer.Equals(that.Authorizer)
+}
+
+func (ah *AuthorizerHolder) Sanitize() cloud.Config {
+	return &AuthorizerHolder{Authorizer: ah.Authorizer.Sanitize().(Authorizer)}
+}
+
+func (ah *AuthorizerHolder) GetBlobClient(serviceURL string) (*azblob.Client, error) {
+	// Create a default request pipeline using your storage account name and account key.
+	cred, err := ah.GetCredential()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := azblob.NewClient(serviceURL, cred, nil)
+	return client, err
+}

+ 27 - 22
pkg/cloud/azure/storagebillingparser.go

@@ -9,7 +9,8 @@ import (
 	"strings"
 	"time"
 
-	"github.com/Azure/azure-storage-blob-go/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
 	"github.com/opencost/opencost/pkg/cloud"
 	"github.com/opencost/opencost/pkg/log"
 )
@@ -36,13 +37,14 @@ func (asbp *AzureStorageBillingParser) ParseBillingData(start, end time.Time, re
 		return err
 	}
 
-	containerURL, err := asbp.getContainer()
+	serviceURL := fmt.Sprintf(asbp.StorageConnection.getBlobURLTemplate(), asbp.Account, "")
+	client, err := asbp.Authorizer.GetBlobClient(serviceURL)
 	if err != nil {
 		asbp.ConnectionStatus = cloud.FailedConnection
 		return err
 	}
 	ctx := context.Background()
-	blobNames, err := asbp.getMostRecentBlobs(start, end, containerURL, ctx)
+	blobNames, err := asbp.getMostRecentBlobs(start, end, client, ctx)
 	if err != nil {
 		asbp.ConnectionStatus = cloud.FailedConnection
 		return err
@@ -54,7 +56,7 @@ func (asbp *AzureStorageBillingParser) ParseBillingData(start, end time.Time, re
 	}
 
 	for _, blobName := range blobNames {
-		blobBytes, err2 := asbp.DownloadBlob(blobName, containerURL, ctx)
+		blobBytes, err2 := asbp.DownloadBlob(blobName, client, ctx)
 		if err2 != nil {
 			asbp.ConnectionStatus = cloud.FailedConnection
 			return err2
@@ -101,7 +103,7 @@ func (asbp *AzureStorageBillingParser) parseCSV(start, end time.Time, reader *cs
 	return nil
 }
 
-func (asbp *AzureStorageBillingParser) getMostRecentBlobs(start, end time.Time, containerURL *azblob.ContainerURL, ctx context.Context) ([]string, error) {
+func (asbp *AzureStorageBillingParser) getMostRecentBlobs(start, end time.Time, client *azblob.Client, ctx context.Context) ([]string, error) {
 	log.Infof("Azure Storage: retrieving most recent reports from: %v - %v", start, end)
 
 	// Get list of month substrings for months contained in the start to end range
@@ -109,33 +111,36 @@ func (asbp *AzureStorageBillingParser) getMostRecentBlobs(start, end time.Time,
 	if err != nil {
 		return nil, err
 	}
-	mostResentBlobs := make(map[string]azblob.BlobItemInternal)
-	for marker := (azblob.Marker{}); marker.NotDone(); {
-		// Get a result segment starting with the blob indicated by the current Marker.
-		listBlob, err := containerURL.ListBlobsFlatSegment(ctx, marker, azblob.ListBlobsSegmentOptions{})
+	mostResentBlobs := make(map[string]container.BlobItem)
+
+	pager := client.NewListBlobsFlatPager(asbp.Container, &azblob.ListBlobsFlatOptions{
+		Include: container.ListBlobsInclude{Deleted: false, Versions: false},
+	})
+
+	for pager.More() {
+		resp, err := pager.NextPage(ctx)
 		if err != nil {
 			return nil, err
 		}
 
-		// ListBlobs returns the start of the next segment; you MUST use this to get
-		// the next segment (after processing the current result segment).
-		marker = listBlob.NextMarker
-
 		// Using the list of months strings find the most resent blob for each month in the range
-		for _, blobInfo := range listBlob.Segment.BlobItems {
+		for _, blobInfo := range resp.Segment.BlobItems {
+			if blobInfo.Name == nil {
+				continue
+			}
+			// If Container Path configuration exists, check if it is in the blobs name
+			if asbp.Path != "" && !strings.Contains(*blobInfo.Name, asbp.Path) {
+				continue
+			}
 			for _, month := range monthStrs {
-				if strings.Contains(blobInfo.Name, month) {
-					// If Container Path configuration exists, check if it is in the blobs name
-					if asbp.Path != "" && !strings.Contains(blobInfo.Name, asbp.Path) {
-						continue
-					}
-
+				if strings.Contains(*blobInfo.Name, month) {
+					// check if blob is the newest seen for this month
 					if prevBlob, ok := mostResentBlobs[month]; ok {
 						if prevBlob.Properties.CreationTime.After(*blobInfo.Properties.CreationTime) {
 							continue
 						}
 					}
-					mostResentBlobs[month] = blobInfo
+					mostResentBlobs[month] = *blobInfo
 				}
 			}
 		}
@@ -145,7 +150,7 @@ func (asbp *AzureStorageBillingParser) getMostRecentBlobs(start, end time.Time,
 	var blobNames []string
 	for _, month := range monthStrs {
 		if blob, ok := mostResentBlobs[month]; ok {
-			blobNames = append(blobNames, blob.Name)
+			blobNames = append(blobNames, *blob.Name)
 		}
 	}
 

+ 10 - 10
pkg/cloud/azure/storageconfiguration.go

@@ -9,12 +9,12 @@ import (
 )
 
 type StorageConfiguration struct {
-	SubscriptionID string     `json:"subscriptionID"`
-	Account        string     `json:"account"`
-	Container      string     `json:"container"`
-	Path           string     `json:"path"`
-	Cloud          string     `json:"cloud"`
-	Authorizer     Authorizer `json:"authorizer"`
+	SubscriptionID string            `json:"subscriptionID"`
+	Account        string            `json:"account"`
+	Container      string            `json:"container"`
+	Path           string            `json:"path"`
+	Cloud          string            `json:"cloud"`
+	Authorizer     StorageAuthorizer `json:"authorizer"`
 }
 
 // Check ensures that all required fields are set, and throws an error if they are not
@@ -93,7 +93,7 @@ func (sc *StorageConfiguration) Sanitize() cloud.Config {
 		Container:      sc.Container,
 		Path:           sc.Path,
 		Cloud:          sc.Cloud,
-		Authorizer:     sc.Authorizer.Sanitize().(Authorizer),
+		Authorizer:     sc.Authorizer.Sanitize().(StorageAuthorizer),
 	}
 }
 
@@ -153,7 +153,7 @@ func (sc *StorageConfiguration) UnmarshalJSON(b []byte) error {
 	if !ok {
 		return fmt.Errorf("StorageConfiguration: UnmarshalJSON: missing authorizer")
 	}
-	authorizer, err := cloud.AuthorizerFromInterface(authAny, SelectAuthorizerByType)
+	authorizer, err := cloud.AuthorizerFromInterface(authAny, SelectStorageAuthorizerByType)
 	if err != nil {
 		return fmt.Errorf("StorageConfiguration: UnmarshalJSON: %s", err.Error())
 	}
@@ -167,8 +167,8 @@ func ConvertAzureStorageConfigToConfig(asc AzureStorageConfig) cloud.KeyedConfig
 		return nil
 	}
 
-	var authorizer Authorizer
-	authorizer = &AccessKey{
+	var authorizer StorageAuthorizer
+	authorizer = &SharedKeyCredential{
 		AccessKey: asc.AccessKey,
 		Account:   asc.AccountName,
 	}

+ 86 - 28
pkg/cloud/azure/storageconfiguration_test.go

@@ -14,14 +14,14 @@ func TestStorageConfiguration_Validate(t *testing.T) {
 		config   StorageConfiguration
 		expected error
 	}{
-		"valid config Azure AccessKey": {
+		"valid config Azure SharedKeyCredential": {
 			config: StorageConfiguration{
 				SubscriptionID: "subscriptionID",
 				Account:        "account",
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -35,11 +35,11 @@ func TestStorageConfiguration_Validate(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					Account: "account",
 				},
 			},
-			expected: fmt.Errorf("AccessKey: missing access key"),
+			expected: fmt.Errorf("SharedKeyCredential: missing access key"),
 		},
 		"missing authorizer": {
 			config: StorageConfiguration{
@@ -59,7 +59,7 @@ func TestStorageConfiguration_Validate(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -73,7 +73,7 @@ func TestStorageConfiguration_Validate(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -87,7 +87,7 @@ func TestStorageConfiguration_Validate(t *testing.T) {
 				Container:      "",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -101,7 +101,7 @@ func TestStorageConfiguration_Validate(t *testing.T) {
 				Container:      "container",
 				Path:           "",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -115,7 +115,7 @@ func TestStorageConfiguration_Validate(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -155,7 +155,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -166,14 +166,36 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
 			},
 			expected: true,
 		},
-
+		"matching config AuthorizerHolder": {
+			left: StorageConfiguration{
+				SubscriptionID: "subscriptionID",
+				Account:        "account",
+				Container:      "container",
+				Path:           "path",
+				Cloud:          "cloud",
+				Authorizer: &AuthorizerHolder{
+					Authorizer: &DefaultAzureCredentialHolder{},
+				},
+			},
+			right: &StorageConfiguration{
+				SubscriptionID: "subscriptionID",
+				Account:        "account",
+				Container:      "container",
+				Path:           "path",
+				Cloud:          "cloud",
+				Authorizer: &AuthorizerHolder{
+					Authorizer: &DefaultAzureCredentialHolder{},
+				},
+			},
+			expected: true,
+		},
 		"missing both authorizer": {
 			left: StorageConfiguration{
 				SubscriptionID: "subscriptionID",
@@ -208,7 +230,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -222,7 +244,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -237,6 +259,30 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 			},
 			expected: false,
 		},
+		"differing storage authorizer": {
+			left: StorageConfiguration{
+				SubscriptionID: "subscriptionID",
+				Account:        "account",
+				Container:      "container",
+				Path:           "path",
+				Cloud:          "cloud",
+				Authorizer: &SharedKeyCredential{
+					AccessKey: "accessKey",
+					Account:   "account",
+				},
+			},
+			right: &StorageConfiguration{
+				SubscriptionID: "subscriptionID",
+				Account:        "account",
+				Container:      "container",
+				Path:           "path",
+				Cloud:          "cloud",
+				Authorizer: &AuthorizerHolder{
+					Authorizer: &DefaultAzureCredentialHolder{},
+				},
+			},
+			expected: false,
+		},
 		"different subscriptionID": {
 			left: StorageConfiguration{
 				SubscriptionID: "subscriptionID",
@@ -244,7 +290,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -255,7 +301,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -269,7 +315,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -280,7 +326,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -294,7 +340,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -305,7 +351,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container2",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -319,7 +365,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -330,7 +376,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path2",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -344,7 +390,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -355,7 +401,7 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud2",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
@@ -369,12 +415,12 @@ func TestStorageConfiguration_Equals(t *testing.T) {
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
 			},
-			right: &AccessKey{
+			right: &SharedKeyCredential{
 				AccessKey: "accessKey",
 				Account:   "account",
 			},
@@ -409,19 +455,31 @@ func TestStorageConfiguration_JSON(t *testing.T) {
 				Authorizer:     nil,
 			},
 		},
-		"AccessKey Authorizer": {
+		"SharedKeyCredential Authorizer": {
 			config: StorageConfiguration{
 				SubscriptionID: "subscriptionID",
 				Account:        "account",
 				Container:      "container",
 				Path:           "path",
 				Cloud:          "cloud",
-				Authorizer: &AccessKey{
+				Authorizer: &SharedKeyCredential{
 					AccessKey: "accessKey",
 					Account:   "account",
 				},
 			},
 		},
+		"Default AuthorizerHolder Authorizer": {
+			config: StorageConfiguration{
+				SubscriptionID: "subscriptionID",
+				Account:        "account",
+				Container:      "container",
+				Path:           "path",
+				Cloud:          "cloud",
+				Authorizer: &AuthorizerHolder{
+					Authorizer: &DefaultAzureCredentialHolder{},
+				},
+			},
+		},
 	}
 
 	for name, testCase := range testCases {

+ 10 - 28
pkg/cloud/azure/storageconnection.go

@@ -4,10 +4,9 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"net/url"
 	"strings"
 
-	"github.com/Azure/azure-storage-blob-go/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
 	"github.com/opencost/opencost/pkg/cloud"
 	"github.com/opencost/opencost/pkg/log"
 )
@@ -35,25 +34,6 @@ func (sc *StorageConnection) Equals(config cloud.Config) bool {
 	return sc.StorageConfiguration.Equals(&thatConfig.StorageConfiguration)
 }
 
-func (sc *StorageConnection) getContainer() (*azblob.ContainerURL, error) {
-
-	credential, err := sc.Authorizer.GetBlobCredentials()
-	if err != nil {
-		return nil, err
-	}
-
-	p := azblob.NewPipeline(credential, azblob.PipelineOptions{})
-
-	// From the Azure portal, get your storage account blob service URL endpoint.
-	URL, _ := url.Parse(
-		fmt.Sprintf(sc.getBlobURLTemplate(), sc.Account, sc.Container))
-
-	// Create a ContainerURL object that wraps the container URL and a request
-	// pipeline to make requests.
-	containerURL := azblob.NewContainerURL(*URL, p)
-	return &containerURL, nil
-}
-
 // getBlobURLTemplate returns the correct BlobUrl for whichever Cloud storage account is specified by the AzureCloud configuration
 // defaults to the Public Cloud template
 func (sc *StorageConnection) getBlobURLTemplate() string {
@@ -65,22 +45,24 @@ func (sc *StorageConnection) getBlobURLTemplate() string {
 	return "https://%s.blob.core.windows.net/%s"
 }
 
-func (sc *StorageConnection) DownloadBlob(blobName string, containerURL *azblob.ContainerURL, ctx context.Context) ([]byte, error) {
+func (sc *StorageConnection) DownloadBlob(blobName string, client *azblob.Client, ctx context.Context) ([]byte, error) {
 	log.Infof("Azure Storage: retrieving blob: %v", blobName)
 
-	blobURL := containerURL.NewBlobURL(blobName)
-	downloadResponse, err := blobURL.Download(ctx, 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
+	downloadResponse, err := client.DownloadStream(ctx, sc.Container, blobName, nil)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Azure: DownloadBlob: failed to download %w", err)
 	}
 	// NOTE: automatically retries are performed if the connection fails
-	bodyStream := downloadResponse.Body(azblob.RetryReaderOptions{MaxRetryRequests: 20})
+	retryReader := downloadResponse.NewRetryReader(ctx, &azblob.RetryReaderOptions{})
+	defer retryReader.Close()
 
 	// read the body into a buffer
 	downloadedData := bytes.Buffer{}
-	_, err = downloadedData.ReadFrom(bodyStream)
+
+	_, err = downloadedData.ReadFrom(retryReader)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Azure: DownloadBlob: failed to read downloaded data %w", err)
 	}
+
 	return downloadedData.Bytes(), nil
 }

+ 3 - 3
pkg/cloud/config/configurations_test.go

@@ -31,7 +31,7 @@ var (
 					Container:      "containerName",
 					Path:           "containerPath",
 					Cloud:          "azureCloud",
-					Authorizer: &azure.AccessKey{
+					Authorizer: &azure.SharedKeyCredential{
 						AccessKey: "accessKey",
 						Account:   "accountName",
 					},
@@ -214,11 +214,11 @@ func TestConfigurations_UnmarshalJSON(t *testing.T) {
 		input    any
 		expected *Configurations
 	}{
-		"Azure Storage AccessKey": {
+		"Azure Storage SharedKeyCredential": {
 			input:    azureConfiguration,
 			expected: azureConfiguration,
 		},
-		"Azure Storage AccessKey Conversion": {
+		"Azure Storage SharedKeyCredential Conversion": {
 			input:    azureMultiCloudConf,
 			expected: azureConfiguration,
 		},

+ 1 - 1
pkg/cmd/costmodel/costmodel.go

@@ -78,7 +78,7 @@ func Execute(opts *CostModelOpts) error {
 	telemetryHandler := metrics.ResponseMetricMiddleware(rootMux)
 	handler := cors.AllowAll().Handler(telemetryHandler)
 
-	return http.ListenAndServe(":9003", errors.PanicHandlerMiddleware(handler))
+	return http.ListenAndServe(fmt.Sprint(":", env.GetAPIPort()), errors.PanicHandlerMiddleware(handler))
 }
 
 func StartExportWorker(ctx context.Context, model costmodel.AllocationModel) error {

+ 8 - 0
pkg/env/costmodelenv.go

@@ -11,6 +11,8 @@ import (
 )
 
 const (
+	APIPortEnvVar = "API_PORT"
+
 	AWSAccessKeyIDEnvVar     = "AWS_ACCESS_KEY_ID"
 	AWSAccessKeySecretEnvVar = "AWS_SECRET_ACCESS_KEY"
 	AWSClusterIDEnvVar       = "AWS_CLUSTER_ID"
@@ -147,6 +149,12 @@ func GetExportCSVMaxDays() int {
 	return GetInt(ExportCSVMaxDays, 90)
 }
 
+// GetAPIPort returns the environment variable value for APIPortEnvVar which
+// is the port number the API is available on.
+func GetAPIPort() int {
+	return GetInt(APIPortEnvVar, 9003)
+}
+
 // GetKubecostConfigBucket returns a file location for a mounted bucket configuration which is used to store
 // a subset of kubecost configurations that require sharing via remote storage.
 func GetKubecostConfigBucket() string {

+ 38 - 0
pkg/env/costmodelenv_test.go

@@ -5,6 +5,44 @@ import (
 	"testing"
 )
 
+func TestGetAPIPort(t *testing.T) {
+	tests := []struct {
+		name string
+		want int
+		pre  func()
+	}{
+		{
+			name: "Ensure the default API port '9003'",
+			want: 9003,
+		},
+		{
+			name: "Ensure the default API port '9003' when API_PORT is set to ''",
+			want: 9003,
+			pre: func() {
+				os.Setenv("API_PORT", "")
+			},
+		},
+		{
+			name: "Ensure the API port '9004' when API_PORT is set to '9004'",
+			want: 9004,
+			pre: func() {
+				os.Setenv("API_PORT", "9004")
+			},
+		},
+	}
+	for _, tt := range tests {
+		if tt.pre != nil {
+			tt.pre()
+		}
+		t.Run(tt.name, func(t *testing.T) {
+			if got := GetAPIPort(); got != tt.want {
+				t.Errorf("GetAPIPort() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+
+}
+
 func TestIsCacheDisabled(t *testing.T) {
 	tests := []struct {
 		name string

+ 5 - 1
ui/Dockerfile

@@ -7,8 +7,12 @@ RUN npx parcel build src/index.html
 
 FROM nginx:alpine
 
+ENV API_PORT=9003
+ENV API_SERVER=0.0.0.0
+ENV UI_PORT=9090
+
 COPY --from=builder /opt/ui/dist /var/www
-COPY default.nginx.conf /etc/nginx/conf.d/
+COPY default.nginx.conf.template /etc/nginx/conf.d/default.nginx.conf.template
 COPY nginx.conf /etc/nginx/
 COPY ./docker-entrypoint.sh /usr/local/bin/
 

+ 4 - 0
ui/Dockerfile.cross

@@ -1,5 +1,9 @@
 FROM nginx:alpine
 
+ENV API_PORT=9003
+ENV API_SERVER=0.0.0.0
+ENV UI_PORT=9090
+
 COPY ./dist /var/www
 COPY default.nginx.conf /etc/nginx/conf.d/
 COPY nginx.conf /etc/nginx/

+ 3 - 3
ui/default.nginx.conf → ui/default.nginx.conf.template

@@ -35,7 +35,7 @@ gzip_types
 upstream model {
     # Update to the cost model endpoint
     # Example: host.docker.internal:9003;
-    server 0.0.0.0:9003;
+    server ${API_SERVER}:${API_PORT};
 }
 
 server {
@@ -58,8 +58,8 @@ server {
     }
 
     add_header ETag "1.96.0";
-    listen 9090;
-    listen [::]:9090;
+    listen ${UI_PORT};
+    listen [::]:${UI_PORT};
     resolver 127.0.0.1 valid=5s;
     location /healthz {
         access_log /dev/null;

+ 4 - 2
ui/docker-entrypoint.sh

@@ -4,10 +4,12 @@ set -e
 if [[ ! -z "$BASE_URL_OVERRIDE" ]]; then
     echo "running with BASE_URL=${BASE_URL_OVERRIDE}"
     sed -i "s^{PLACEHOLDER_BASE_URL}^$BASE_URL_OVERRIDE^g" /var/www/*.js
-else 
+else
     echo "running with BASE_URL=${BASE_URL}"
     sed -i "s^{PLACEHOLDER_BASE_URL}^$BASE_URL^g" /var/www/*.js
 fi
 
+envsubst '$API_PORT $API_SERVER $UI_PORT' < /etc/nginx/conf.d/default.nginx.conf.template > /etc/nginx/conf.d/default.nginx.conf
+
 # Run the parent (nginx) container's entrypoint script
-exec /docker-entrypoint.sh "$@"
+exec /docker-entrypoint.sh "$@"