diff --git a/cli/azd/pkg/environment/azdcontext/azdcontext.go b/cli/azd/pkg/environment/azdcontext/azdcontext.go index 1b8f46a8e97..f3a32d31f06 100644 --- a/cli/azd/pkg/environment/azdcontext/azdcontext.go +++ b/cli/azd/pkg/environment/azdcontext/azdcontext.go @@ -19,6 +19,7 @@ const EnvironmentDirectoryName = ".azure" const DotEnvFileName = ".env" const ConfigFileName = "config.json" const ConfigFileVersion = 1 +const EnvironmentInfraDirectoryName = "infra" type AzdContext struct { projectDirectory string @@ -53,6 +54,26 @@ func (c *AzdContext) GetEnvironmentWorkDirectory(name string) string { return filepath.Join(c.EnvironmentRoot(name), "wd") } +func (c *AzdContext) GetEnvironmentInfraDirectory(name string) string { + return filepath.Join(c.EnvironmentRoot(name), EnvironmentInfraDirectoryName) +} + +func (c *AzdContext) GetEnvironmentInfraFiles(name string) []string { + var files []string + infraDir := c.GetEnvironmentInfraDirectory(name) + + entries, err := os.ReadDir(infraDir) + if err == nil { + for _, entry := range entries { + if !entry.IsDir() { + files = append(files, filepath.Join(infraDir, entry.Name())) + } + } + } + + return files +} + // GetDefaultEnvironmentName returns the name of the default environment. Returns // an empty string if a default environment has not been set. func (c *AzdContext) GetDefaultEnvironmentName() (string, error) { diff --git a/cli/azd/pkg/environment/manager.go b/cli/azd/pkg/environment/manager.go index 1fd93ded735..650b4c7fdeb 100644 --- a/cli/azd/pkg/environment/manager.go +++ b/cli/azd/pkg/environment/manager.go @@ -43,6 +43,7 @@ type Spec struct { const DotEnvFileName = ".env" const ConfigFileName = "config.json" +const InfraFilesDir = "infra" var ( // Error returned when an environment with the specified name already exists diff --git a/cli/azd/pkg/environment/storage_blob_data_store.go b/cli/azd/pkg/environment/storage_blob_data_store.go index 22b70eabc30..9357bc38896 100644 --- a/cli/azd/pkg/environment/storage_blob_data_store.go +++ b/cli/azd/pkg/environment/storage_blob_data_store.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "io" "os" "path/filepath" "slices" @@ -19,6 +20,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azsdk/storage" "github.com/azure/azure-dev/cli/azd/pkg/config" "github.com/azure/azure-dev/cli/azd/pkg/contracts" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" "github.com/google/uuid" "github.com/joho/godotenv" ) @@ -31,12 +33,15 @@ var ( type StorageBlobDataStore struct { configManager config.Manager blobClient storage.BlobClient + azdContext *azdcontext.AzdContext } -func NewStorageBlobDataStore(configManager config.Manager, blobClient storage.BlobClient) RemoteDataStore { +func NewStorageBlobDataStore(configManager config.Manager, blobClient storage.BlobClient, + azdContext *azdcontext.AzdContext) RemoteDataStore { return &StorageBlobDataStore{ configManager: configManager, blobClient: blobClient, + azdContext: azdContext, } } @@ -50,6 +55,11 @@ func (fs *StorageBlobDataStore) ConfigPath(env *Environment) string { return fmt.Sprintf("%s/%s", env.name, ConfigFileName) } +// InfraPath returns the path to the infra files for the given environment +func (fs *StorageBlobDataStore) InfraPath(env *Environment) string { + return fmt.Sprintf("%s/%s", env.name, InfraFilesDir) +} + func (sbd *StorageBlobDataStore) List(ctx context.Context) ([]*contracts.EnvListEnvironment, error) { blobs, err := sbd.blobClient.Items(ctx) if err != nil { @@ -143,6 +153,22 @@ func (sbd *StorageBlobDataStore) Save(ctx context.Context, env *Environment, opt return fmt.Errorf("uploading .env: %w", describeError(err)) } + // Upload Infra Files if any + filesToUpload := sbd.azdContext.GetEnvironmentInfraFiles(env.name) + + for _, file := range filesToUpload { + fileName := filepath.Base(file) + fileBuffer, err := os.Open(file) + if err != nil { + return fmt.Errorf("failed to open file for upload: %w", err) + } + defer fileBuffer.Close() + err = sbd.blobClient.Upload(ctx, fmt.Sprintf("%s/%s/%s", env.name, InfraFilesDir, fileName), fileBuffer) + if err != nil { + return fmt.Errorf("uploading infra file: %w", err) + } + } + tracing.SetUsageAttributes(fields.StringHashed(fields.EnvNameKey, env.Name())) return nil } @@ -191,6 +217,41 @@ func (sbd *StorageBlobDataStore) Reload(ctx context.Context, env *Environment) e tracing.SetGlobalAttributes(fields.StringHashed(fields.SubscriptionIdKey, env.GetSubscriptionId())) } + // Reload infra config file if any + items, err := sbd.blobClient.Items(ctx) + if err != nil { + return describeError(err) + } + + for _, item := range items { + if strings.Contains(item.Path, sbd.InfraPath(env)) { + + blobStream, err := sbd.blobClient.Download(ctx, item.Path) + if err != nil { + return fmt.Errorf("failed to download blob: %w", err) + } + defer blobStream.Close() + + localInfraDir := sbd.azdContext.GetEnvironmentInfraDirectory(env.name) + err = os.MkdirAll(localInfraDir, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + localFilePath := fmt.Sprintf("%s/%s", localInfraDir, filepath.Base(item.Path)) + file, err := os.Create(localFilePath) + if err != nil { + return fmt.Errorf("failed to create local file: %w", err) + } + defer file.Close() + + _, err = io.Copy(file, blobStream) + if err != nil { + return fmt.Errorf("failed to write blob to local file: %w", err) + } + } + } + return nil } diff --git a/cli/azd/pkg/environment/storage_blob_data_store_test.go b/cli/azd/pkg/environment/storage_blob_data_store_test.go index 32aa1afdb36..68737846f57 100644 --- a/cli/azd/pkg/environment/storage_blob_data_store_test.go +++ b/cli/azd/pkg/environment/storage_blob_data_store_test.go @@ -12,6 +12,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azsdk/storage" "github.com/azure/azure-dev/cli/azd/pkg/config" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" "github.com/azure/azure-dev/cli/azd/test/mocks" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -39,11 +40,12 @@ var validBlobItems []*storage.Blob = []*storage.Blob{ func Test_StorageBlobDataStore_List(t *testing.T) { mockContext := mocks.NewMockContext(context.Background()) configManager := config.NewManager() + azdContext := azdcontext.NewAzdContextWithDirectory(t.TempDir()) t.Run("List", func(t *testing.T) { blobClient := &MockBlobClient{} blobClient.On("Items", *mockContext.Context).Return(validBlobItems, nil) - dataStore := NewStorageBlobDataStore(configManager, blobClient) + dataStore := NewStorageBlobDataStore(configManager, blobClient, azdContext) envList, err := dataStore.List(*mockContext.Context) require.NoError(t, err) @@ -56,7 +58,7 @@ func Test_StorageBlobDataStore_List(t *testing.T) { t.Run("Empty", func(t *testing.T) { blobClient := &MockBlobClient{} blobClient.On("Items", *mockContext.Context).Return(nil, storage.ErrContainerNotFound) - dataStore := NewStorageBlobDataStore(configManager, blobClient) + dataStore := NewStorageBlobDataStore(configManager, blobClient, azdContext) envList, err := dataStore.List(*mockContext.Context) require.NoError(t, err) @@ -69,7 +71,8 @@ func Test_StorageBlobDataStore_SaveAndGet(t *testing.T) { mockContext := mocks.NewMockContext(context.Background()) configManager := config.NewManager() blobClient := &MockBlobClient{} - dataStore := NewStorageBlobDataStore(configManager, blobClient) + azdContext := azdcontext.NewAzdContextWithDirectory(t.TempDir()) + dataStore := NewStorageBlobDataStore(configManager, blobClient, azdContext) t.Run("Success", func(t *testing.T) { envReader := io.NopCloser(bytes.NewReader([]byte("key1=value1"))) @@ -96,7 +99,8 @@ func Test_StorageBlobDataStore_SaveAndGet(t *testing.T) { func Test_StorageBlobDataStore_Path(t *testing.T) { configManager := config.NewManager() blobClient := &MockBlobClient{} - dataStore := NewStorageBlobDataStore(configManager, blobClient) + azdContext := azdcontext.NewAzdContextWithDirectory(t.TempDir()) + dataStore := NewStorageBlobDataStore(configManager, blobClient, azdContext) env := New("env1") expected := fmt.Sprintf("%s/%s", env.name, DotEnvFileName) @@ -108,7 +112,8 @@ func Test_StorageBlobDataStore_Path(t *testing.T) { func Test_StorageBlobDataStore_ConfigPath(t *testing.T) { configManager := config.NewManager() blobClient := &MockBlobClient{} - dataStore := NewStorageBlobDataStore(configManager, blobClient) + azdContext := azdcontext.NewAzdContextWithDirectory(t.TempDir()) + dataStore := NewStorageBlobDataStore(configManager, blobClient, azdContext) env := New("env1") expected := fmt.Sprintf("%s/%s", env.name, ConfigFileName)