diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index 26fbde3a07e..384ed1e32ca 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -5,6 +5,7 @@ package project import ( "context" + "errors" "fmt" "io/fs" "log" @@ -15,6 +16,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/alpha" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" + "github.com/otiai10/copy" ) type ImportManager struct { @@ -139,8 +141,63 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi infraRoot = filepath.Join(projectConfig.Path, infraRoot) } + moduleExists, moduleErr := pathHasModule(infraRoot, projectConfig.Infra.Module) + + composeEnabled := im.dotNetImporter.alphaFeatureManager.IsEnabled(featureCompose) + if composeEnabled && len(projectConfig.Resources) > 0 { + if moduleErr == nil && moduleExists { + azdModuleExists, err := pathHasModule(filepath.Join(infraRoot, "azd"), projectConfig.Infra.Module) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("checking if module exists: %w", err) + } + + if azdModuleExists { + log.Printf("using fully-synthesized infrastructure from %s directory", infraRoot) + return &Infra{ + Options: projectConfig.Infra, + }, nil + } + } + + // copy the infra directory to a temporary directory and synthesize the azd directory + tmpDir, err := os.MkdirTemp("", "azd-infra") + if err != nil { + return nil, fmt.Errorf("creating temporary directory: %w", err) + } + + azdInfraDir := tmpDir + if moduleErr == nil && moduleExists { + // Copy the base infra directory + if err := copy.Copy(infraRoot, tmpDir); err != nil { + return nil, fmt.Errorf("copying infra directory: %w", err) + } + + azdInfraDir = filepath.Join(tmpDir, "azd") + } + + err = infraFsToDir(ctx, projectConfig, azdInfraDir) + if err != nil { + return nil, err + } + + return &Infra{ + Options: provisioning.Options{ + Provider: provisioning.Bicep, + Path: tmpDir, + Module: DefaultModule, + }, + cleanupDir: tmpDir, + }, nil + } + + if !composeEnabled && len(projectConfig.Resources) > 0 { + return nil, fmt.Errorf( + "compose is currently under alpha support and must be explicitly enabled."+ + " Run `%s` to enable this feature", alpha.GetEnableCommand(featureCompose)) + } + // Allow overriding the infrastructure only when path and module exists. - if moduleExists, err := pathHasModule(infraRoot, projectConfig.Infra.Module); err == nil && moduleExists { + if moduleErr == nil && moduleExists { log.Printf("using infrastructure from %s directory", infraRoot) return &Infra{ Options: projectConfig.Infra, @@ -165,17 +222,6 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi } } - composeEnabled := im.dotNetImporter.alphaFeatureManager.IsEnabled(featureCompose) - if composeEnabled && len(projectConfig.Resources) > 0 { - return tempInfra(ctx, projectConfig) - } - - if !composeEnabled && len(projectConfig.Resources) > 0 { - return nil, fmt.Errorf( - "compose is currently under alpha support and must be explicitly enabled."+ - " Run `%s` to enable this feature", alpha.GetEnableCommand(featureCompose)) - } - return &Infra{}, nil } diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index 168e5c93261..594da660006 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -233,7 +233,8 @@ func TestImportManagerProjectInfrastructure(t *testing.T) { lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { return mockEnv, nil }), - hostCheck: make(map[string]hostCheckResult), + hostCheck: make(map[string]hostCheckResult), + alphaFeatureManager: mockContext.AlphaFeaturesManager, }) // Do not use defaults diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 120f1c63211..1ce8436ab8c 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -5,6 +5,7 @@ package project import ( "context" + "errors" "fmt" "io/fs" "os" @@ -13,7 +14,6 @@ import ( "strings" "github.com/azure/azure-dev/cli/azd/internal/scaffold" - "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/psanford/memfs" ) @@ -38,18 +38,10 @@ func infraFs(_ context.Context, prjConfig *ProjectConfig) (fs.FS, error) { return files, nil } -// Returns the infrastructure configuration that points to a temporary, generated `infra` directory on the filesystem. -func tempInfra( - ctx context.Context, - prjConfig *ProjectConfig) (*Infra, error) { - tmpDir, err := os.MkdirTemp("", "azd-infra") - if err != nil { - return nil, fmt.Errorf("creating temporary directory: %w", err) - } - +func infraFsToDir(ctx context.Context, prjConfig *ProjectConfig, dir string) error { files, err := infraFs(ctx, prjConfig) if err != nil { - return nil, err + return err } err = fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error { @@ -61,7 +53,7 @@ func tempInfra( return nil } - target := filepath.Join(tmpDir, path) + target := filepath.Join(dir, path) if err := os.MkdirAll(filepath.Dir(target), osutil.PermissionDirectoryOwnerOnly); err != nil { return err } @@ -74,17 +66,10 @@ func tempInfra( return os.WriteFile(target, contents, d.Type().Perm()) }) if err != nil { - return nil, fmt.Errorf("writing infrastructure: %w", err) + return fmt.Errorf("writing infrastructure: %w", err) } - return &Infra{ - Options: provisioning.Options{ - Provider: provisioning.Bicep, - Path: tmpDir, - Module: DefaultModule, - }, - cleanupDir: tmpDir, - }, nil + return nil } // Generates the filesystem of all infrastructure files to be placed, rooted at the project directory. @@ -95,13 +80,32 @@ func infraFsForProject(ctx context.Context, prjConfig *ProjectConfig) (fs.FS, er return nil, err } - infraPathPrefix := DefaultPath + infraPrefix := DefaultPath if prjConfig.Infra.Path != "" { - infraPathPrefix = prjConfig.Infra.Path + infraPrefix = prjConfig.Infra.Path + } + + infraRoot := infraPrefix + if !filepath.IsAbs(infraPrefix) { + infraRoot = filepath.Join(prjConfig.Path, infraPrefix) + } + + infraDir, err := os.Stat(infraRoot) + if !errors.Is(err, os.ErrNotExist) && err != nil { + return nil, fmt.Errorf("error reading infra directory: %w", err) + } + + fi, err := os.Stat(filepath.Join(infraRoot, ".azd")) + if !errors.Is(err, os.ErrNotExist) && err != nil { + return nil, fmt.Errorf("error reading .azd file in infra: %w", err) + } + + if infraDir != nil && fi == nil { // if the infra directory is not managed by azd, generate it to infra/azd + infraPrefix = filepath.Join(infraPrefix, "azd") } - // root the generated content at the project directory generatedFS := memfs.New() + // root the generated content at the project directory err = fs.WalkDir(infraFS, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err @@ -111,7 +115,7 @@ func infraFsForProject(ctx context.Context, prjConfig *ProjectConfig) (fs.FS, er return nil } - err = generatedFS.MkdirAll(filepath.Join(infraPathPrefix, filepath.Dir(path)), osutil.PermissionDirectoryOwnerOnly) + err = generatedFS.MkdirAll(filepath.Join(infraPrefix, filepath.Dir(path)), osutil.PermissionDirectoryOwnerOnly) if err != nil { return err } @@ -121,10 +125,18 @@ func infraFsForProject(ctx context.Context, prjConfig *ProjectConfig) (fs.FS, er return err } - return generatedFS.WriteFile(filepath.Join(infraPathPrefix, path), contents, d.Type().Perm()) + return generatedFS.WriteFile(filepath.Join(infraPrefix, path), contents, d.Type().Perm()) }) if err != nil { - return nil, err + return nil, fmt.Errorf("generating: %w", err) + } + + if fi == nil { + // create a sentinel file to indicate that the infra directory is managed by azd + err = generatedFS.WriteFile(filepath.Join(infraPrefix, ".azd"), []byte{}, osutil.PermissionFileOwnerOnly) + if err != nil { + return nil, fmt.Errorf("writing sentinel: %w", err) + } } return generatedFS, nil