From 859ba88cb84109c782654bc11b6ee2ac8891e5ad Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Sat, 9 Dec 2023 01:08:23 +0000 Subject: [PATCH 01/10] check module before using infra defaults --- cli/azd/internal/repository/initializer.go | 5 +- .../provisioning/bicep/bicep_provider.go | 10 ---- cli/azd/pkg/infra/provisioning/manager.go | 14 ++++++ .../terraform/terraform_provider.go | 10 ---- cli/azd/pkg/project/importer.go | 49 +++++++++++++++++-- go.mod | 1 - go.sum | 2 - 7 files changed, 61 insertions(+), 30 deletions(-) diff --git a/cli/azd/internal/repository/initializer.go b/cli/azd/internal/repository/initializer.go index bcf2b414f3c..0aa605b047f 100644 --- a/cli/azd/internal/repository/initializer.go +++ b/cli/azd/internal/repository/initializer.go @@ -14,7 +14,6 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" - "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning/bicep" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/lazy" "github.com/azure/azure-dev/cli/azd/pkg/osutil" @@ -341,7 +340,7 @@ func (i *Initializer) InitializeMinimal(ctx context.Context, azdCtx *azdcontext. // Default infra path if not specified infraPath := projectConfig.Infra.Path if infraPath == "" { - infraPath = bicep.Defaults.Path + infraPath = project.InfraDefaults.Path } err = os.MkdirAll(infraPath, osutil.PermissionDirectory) @@ -351,7 +350,7 @@ func (i *Initializer) InitializeMinimal(ctx context.Context, azdCtx *azdcontext. module := projectConfig.Infra.Module if projectConfig.Infra.Module == "" { - module = bicep.Defaults.Module + module = project.InfraDefaults.Module } mainPath := filepath.Join(infraPath, module) diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index 707120f3179..c81d5db62be 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -19,7 +19,6 @@ import ( "strings" "time" - "dario.cat/mergo" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" @@ -46,11 +45,6 @@ import ( "golang.org/x/exp/maps" ) -var Defaults = Options{ - Module: "main", - Path: "infra", -} - type deploymentDetails struct { CompiledBicep *compileBicepResult // Target is the unique resource in azure that represents the deployment that will happen. A target can be scoped to @@ -91,10 +85,6 @@ func (p *BicepProvider) RequiredExternalTools() []tools.ExternalTool { } func (p *BicepProvider) Initialize(ctx context.Context, projectPath string, options Options) error { - if err := mergo.Merge(&options, Defaults); err != nil { - return fmt.Errorf("merging bicep defaults: %w", err) - } - p.projectPath = projectPath p.options = options diff --git a/cli/azd/pkg/infra/provisioning/manager.go b/cli/azd/pkg/infra/provisioning/manager.go index 78f7bcb48cb..2d6db1c4d90 100644 --- a/cli/azd/pkg/infra/provisioning/manager.go +++ b/cli/azd/pkg/infra/provisioning/manager.go @@ -31,7 +31,21 @@ type Manager struct { options *Options } +// defaultOptions for this package. These values are applied across provisioning providers. +var defaults = Options{ + Module: "main", + Path: "infra", +} + func (m *Manager) Initialize(ctx context.Context, projectPath string, options Options) error { + // applied defaults if missing + if options.Module == "" { + options.Module = defaults.Module + } + if options.Path == "" { + options.Path = defaults.Path + } + m.projectPath = projectPath m.options = &options diff --git a/cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go b/cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go index 79e17dee684..8f1b0132539 100644 --- a/cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go +++ b/cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go @@ -9,7 +9,6 @@ import ( "path/filepath" "strings" - "dario.cat/mergo" "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/account" "github.com/azure/azure-dev/cli/azd/pkg/environment" @@ -24,11 +23,6 @@ import ( "golang.org/x/exp/maps" ) -var Defaults = Options{ - Module: "main", - Path: "infra", -} - // TerraformProvider exposes infrastructure provisioning using Azure Terraform templates type TerraformProvider struct { envManager environment.Manager @@ -78,10 +72,6 @@ func NewTerraformProvider( } func (t *TerraformProvider) Initialize(ctx context.Context, projectPath string, options Options) error { - if err := mergo.Merge(&options, Defaults); err != nil { - return fmt.Errorf("merging terraform defaults: %w", err) - } - t.projectPath = projectPath t.options = options diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index c92d2e96ed5..db3d8436bd0 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -96,15 +96,31 @@ func (im *ImportManager) ServiceStable(ctx context.Context, projectConfig *Proje return allServicesSlice, nil } +// defaultOptions for infra settings. These values are applied across provisioning providers. +var InfraDefaults = provisioning.Options{ + Module: "main", + Path: "infra", +} + +// ProjectInfrastructure parses the project configuration and returns the infrastructure configuration. +// The configuration can be explicitly defined on azure.yaml using path and module, or in case these values +// are not explicitly defined, the project importer uses default values to find the infrastructure. func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfig *ProjectConfig) (*Infra, error) { + // Use default project values for Infra when not specified in azure.yaml + if projectConfig.Infra.Module == "" { + projectConfig.Infra.Module = InfraDefaults.Module + } + if projectConfig.Infra.Path == "" { + projectConfig.Infra.Path = InfraDefaults.Path + } + infraRoot := projectConfig.Infra.Path if !filepath.IsAbs(infraRoot) { infraRoot = filepath.Join(projectConfig.Path, infraRoot) } - // Allow overriding the infrastructure by placing an `infra` folder in the location that would be expected based - // on azure.yaml - if _, err := os.Stat(infraRoot); err == nil { + // Allow overriding the infrastructure only when path and module exists. + if moduleExists, err := pathHasModule(infraRoot, projectConfig.Infra.Module); err == nil && moduleExists { log.Printf("using infrastructure from %s directory", infraRoot) return &Infra{ Options: projectConfig.Infra, @@ -130,7 +146,32 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi } return nil, fmt.Errorf( - "this project does not contain any infrastructure, have you created an '%s' folder?", filepath.Base(infraRoot)) + "this project does not contain expected infrastructure, folder: '%s' and module: '%s'", + infraRoot, + projectConfig.Infra.Module) +} + +func pathHasModule(path, module string) (bool, error) { + var moduleFound bool + err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + fileNameWithoutExt := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name())) + if fileNameWithoutExt == module { + moduleFound = true + return filepath.SkipDir + } + } + return nil + }) + + if err != nil { + return false, fmt.Errorf("error while iterating directory: %w", err) + } + + return moduleFound, nil } func (im *ImportManager) SynthAllInfrastructure(ctx context.Context, projectConfig *ProjectConfig) (fs.FS, error) { diff --git a/go.mod b/go.mod index 5a6d18adbe4..c4c5b483432 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/azure/azure-dev go 1.21 require ( - dario.cat/mergo v1.0.0 github.com/AlecAivazis/survey/v2 v2.3.2 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 diff --git a/go.sum b/go.sum index 2da00437e22..dda2ba09b85 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,6 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8= github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= From fd635ef51aa5586eada0ff44631a5b7f422d50f7 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 11 Dec 2023 18:48:57 +0000 Subject: [PATCH 02/10] fix test --- cli/azd/test/functional/telemetry_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cli/azd/test/functional/telemetry_test.go b/cli/azd/test/functional/telemetry_test.go index 4e951fe42f6..78c07fa9eb8 100644 --- a/cli/azd/test/functional/telemetry_test.go +++ b/cli/azd/test/functional/telemetry_test.go @@ -251,10 +251,15 @@ func Test_CLI_Telemetry_NestedCommands(t *testing.T) { // Remove infra folder to avoid lengthy Azure operations while asserting the intended telemetry behavior. // The current behavior is that `azd provision` will fail when trying to read the nonexistent bicep folder. - require.NoError(t, os.RemoveAll(filepath.Join(dir, "infra"))) + infraPath := filepath.Join(dir, "infra") + require.NoError(t, os.RemoveAll(infraPath)) - // We do require that infra folder exist, however, so put it back (empty). - require.NoError(t, os.MkdirAll(filepath.Join(dir, "infra"), osutil.PermissionDirectoryOwnerOnly)) + // We do require that infra folder exist, however, so put it back with a module which will throw during provisioning. + require.NoError(t, os.MkdirAll(infraPath, osutil.PermissionDirectoryOwnerOnly)) + // main.something will allow azd to continue until trying to find and build bicep. + file, err := os.Create(filepath.Join(infraPath, "main.something")) + require.NoError(t, err) + defer file.Close() _, err = cli.RunCommandWithStdIn(ctx, stdinForProvision(), "up", "--trace-log-file", traceFilePath) require.Error(t, err) From 6b51a52e840863e1e24861189dc8d41cf7c1fef5 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 22 Dec 2023 19:56:42 +0000 Subject: [PATCH 03/10] check only top level --- cli/azd/pkg/project/importer.go | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index 85d23962b9d..a164fbca1b4 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -153,26 +153,17 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi } func pathHasModule(path, module string) (bool, error) { - var moduleFound bool - err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - fileNameWithoutExt := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name())) - if fileNameWithoutExt == module { - moduleFound = true - return filepath.SkipDir - } - } - return nil - }) - + files, err := os.ReadDir(path) if err != nil { return false, fmt.Errorf("error while iterating directory: %w", err) } - return moduleFound, nil + return slices.ContainsFunc(files, func(file fs.DirEntry) bool { + fileName := file.Name() + fileNameNoExt := strings.TrimSuffix(fileName, filepath.Ext(fileName)) + return !file.IsDir() && fileNameNoExt == module + }), nil + } func (im *ImportManager) SynthAllInfrastructure(ctx context.Context, projectConfig *ProjectConfig) (fs.FS, error) { From 6eb5a3e12d52166a70399a627364c882a30e72b3 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 28 Dec 2023 18:28:28 +0000 Subject: [PATCH 04/10] add defaults for providers --- .../pkg/infra/provisioning/bicep/bicep_provider.go | 11 +++++++++++ .../provisioning/terraform/terraform_provider.go | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index caa2a8822b5..27101106d9b 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -45,6 +45,11 @@ import ( "golang.org/x/exp/maps" ) +const ( + defaultModule = "main" + defaultPath = "infra" +) + type deploymentDetails struct { CompiledBicep *compileBicepResult // Target is the unique resource in azure that represents the deployment that will happen. A target can be scoped to @@ -87,6 +92,12 @@ func (p *BicepProvider) RequiredExternalTools() []tools.ExternalTool { func (p *BicepProvider) Initialize(ctx context.Context, projectPath string, options Options) error { p.projectPath = projectPath p.options = options + if p.options.Module == "" { + p.options.Module = defaultModule + } + if p.options.Path == "" { + p.options.Path = defaultPath + } requiredTools := p.RequiredExternalTools() if err := tools.EnsureInstalled(ctx, requiredTools...); err != nil { diff --git a/cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go b/cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go index 9a4b8a91dd5..1b3808c23b0 100644 --- a/cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go +++ b/cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go @@ -23,6 +23,11 @@ import ( "golang.org/x/exp/maps" ) +const ( + defaultModule = "main" + defaultPath = "infra" +) + // TerraformProvider exposes infrastructure provisioning using Azure Terraform templates type TerraformProvider struct { envManager environment.Manager @@ -74,6 +79,12 @@ func NewTerraformProvider( func (t *TerraformProvider) Initialize(ctx context.Context, projectPath string, options Options) error { t.projectPath = projectPath t.options = options + if t.options.Module == "" { + t.options.Module = defaultModule + } + if t.options.Path == "" { + t.options.Path = defaultPath + } requiredTools := t.RequiredExternalTools() if err := tools.EnsureInstalled(ctx, requiredTools...); err != nil { From 4ecf358f38efe9c0977ccdf16b17c7fc370d45b8 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 28 Dec 2023 18:52:04 +0000 Subject: [PATCH 05/10] define defaults for project package --- cli/azd/internal/repository/initializer.go | 4 ++-- cli/azd/pkg/project/importer.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/azd/internal/repository/initializer.go b/cli/azd/internal/repository/initializer.go index 0aa605b047f..2b172d7b077 100644 --- a/cli/azd/internal/repository/initializer.go +++ b/cli/azd/internal/repository/initializer.go @@ -340,7 +340,7 @@ func (i *Initializer) InitializeMinimal(ctx context.Context, azdCtx *azdcontext. // Default infra path if not specified infraPath := projectConfig.Infra.Path if infraPath == "" { - infraPath = project.InfraDefaults.Path + infraPath = project.DefaultPath } err = os.MkdirAll(infraPath, osutil.PermissionDirectory) @@ -350,7 +350,7 @@ func (i *Initializer) InitializeMinimal(ctx context.Context, azdCtx *azdcontext. module := projectConfig.Infra.Module if projectConfig.Infra.Module == "" { - module = project.InfraDefaults.Module + module = project.DefaultModule } mainPath := filepath.Join(infraPath, module) diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index a164fbca1b4..86980967d0c 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -98,10 +98,10 @@ func (im *ImportManager) ServiceStable(ctx context.Context, projectConfig *Proje } // defaultOptions for infra settings. These values are applied across provisioning providers. -var InfraDefaults = provisioning.Options{ - Module: "main", - Path: "infra", -} +const ( + DefaultModule = "main" + DefaultPath = "infra" +) // ProjectInfrastructure parses the project configuration and returns the infrastructure configuration. // The configuration can be explicitly defined on azure.yaml using path and module, or in case these values @@ -109,10 +109,10 @@ var InfraDefaults = provisioning.Options{ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfig *ProjectConfig) (*Infra, error) { // Use default project values for Infra when not specified in azure.yaml if projectConfig.Infra.Module == "" { - projectConfig.Infra.Module = InfraDefaults.Module + projectConfig.Infra.Module = DefaultModule } if projectConfig.Infra.Path == "" { - projectConfig.Infra.Path = InfraDefaults.Path + projectConfig.Infra.Path = DefaultPath } infraRoot := projectConfig.Infra.Path From b3f29f08fd0be9131a2bc099c3c9ae372b239418 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 28 Dec 2023 18:53:43 +0000 Subject: [PATCH 06/10] use const defaults for provisioning manager --- cli/azd/pkg/infra/provisioning/manager.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/azd/pkg/infra/provisioning/manager.go b/cli/azd/pkg/infra/provisioning/manager.go index 2d6db1c4d90..36c6d8f8c12 100644 --- a/cli/azd/pkg/infra/provisioning/manager.go +++ b/cli/azd/pkg/infra/provisioning/manager.go @@ -31,19 +31,19 @@ type Manager struct { options *Options } -// defaultOptions for this package. These values are applied across provisioning providers. -var defaults = Options{ - Module: "main", - Path: "infra", -} +// defaultOptions for this package. +const ( + defaultModule = "main" + defaultPath = "infra" +) func (m *Manager) Initialize(ctx context.Context, projectPath string, options Options) error { // applied defaults if missing if options.Module == "" { - options.Module = defaults.Module + options.Module = defaultModule } if options.Path == "" { - options.Path = defaults.Path + options.Path = defaultPath } m.projectPath = projectPath From 24436dfe8c8d953a59c8966a3b48d68d2346d811 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 30 Jan 2024 05:55:37 +0000 Subject: [PATCH 07/10] adding tests for the project - importer --- cli/azd/pkg/project/dotnet_importer.go | 2 +- cli/azd/pkg/project/importer.go | 3 + cli/azd/pkg/project/importer_test.go | 341 ++++++++++++++++++ cli/azd/pkg/project/project_manager.go | 3 + .../pkg/project/testdata/aspire-escaping.json | 32 ++ 5 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 cli/azd/pkg/project/importer_test.go create mode 100644 cli/azd/pkg/project/testdata/aspire-escaping.json diff --git a/cli/azd/pkg/project/dotnet_importer.go b/cli/azd/pkg/project/dotnet_importer.go index 391500ac3c3..04bddac2c50 100644 --- a/cli/azd/pkg/project/dotnet_importer.go +++ b/cli/azd/pkg/project/dotnet_importer.go @@ -138,7 +138,7 @@ func (ai *DotNetImporter) ProjectInfrastructure(ctx context.Context, svcConfig * Options: provisioning.Options{ Provider: provisioning.Bicep, Path: tmpDir, - Module: "main", + Module: DefaultModule, }, Inputs: inputs, cleanupDir: tmpDir, diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index 86980967d0c..1ea0407f954 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package project import ( diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go new file mode 100644 index 00000000000..224a41708a8 --- /dev/null +++ b/cli/azd/pkg/project/importer_test.go @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package project + +import ( + "context" + _ "embed" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/apphost" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" + "github.com/azure/azure-dev/cli/azd/pkg/lazy" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet" + "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockenv" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestImportManagerHasService(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + mockEnv := &mockenv.MockEnvManager{} + mockEnv.On("Save", mock.Anything, mock.Anything).Return(nil) + + manager := NewImportManager(&DotNetImporter{ + dotnetCli: dotnet.NewDotNetCli(mockContext.CommandRunner), + console: mockContext.Console, + lazyEnv: lazy.NewLazy(func() (*environment.Environment, error) { + return environment.NewWithValues("env", map[string]string{}), nil + }), + lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { + return mockEnv, nil + }), + }) + + // has service + r, e := manager.HasService(*mockContext.Context, &ProjectConfig{ + Services: map[string]*ServiceConfig{ + "test": { + Name: "test", + Language: ServiceLanguageJava, + }, + }, + }, "test") + require.NoError(t, e) + require.True(t, r) + + // has not + r, e = manager.HasService(*mockContext.Context, &ProjectConfig{ + Services: map[string]*ServiceConfig{ + "test": { + Name: "test", + Language: ServiceLanguageJava, + }, + }, + }, "other") + require.NoError(t, e) + require.False(t, r) +} + +func TestImportManagerHasServiceErrorNoMultipleServicesWithAppHost(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + mockEnv := &mockenv.MockEnvManager{} + mockEnv.On("Save", mock.Anything, mock.Anything).Return(nil) + + manager := NewImportManager(&DotNetImporter{ + dotnetCli: dotnet.NewDotNetCli(mockContext.CommandRunner), + console: mockContext.Console, + lazyEnv: lazy.NewLazy(func() (*environment.Environment, error) { + return environment.NewWithValues("env", map[string]string{}), nil + }), + lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { + return mockEnv, nil + }), + hostCheck: make(map[string]hostCheckResult), + }) + + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "dotnet") && + slices.Contains(args.Args, "--getProperty:IsAspireHost") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.RunResult{ + Stdout: "true", + ExitCode: 0, + }, nil + }) + + // errors ** errNoMultipleServicesWithAppHost ** + r, e := manager.HasService(*mockContext.Context, &ProjectConfig{ + Path: "path", + Services: map[string]*ServiceConfig{ + "test": { + Name: "test", + Language: ServiceLanguageDotNet, + RelativePath: "path", + Project: &ProjectConfig{ + Path: "path", + }, + }, + "foo": { + Name: "foo", + Language: ServiceLanguageDotNet, + RelativePath: "path2", + Project: &ProjectConfig{ + Path: "path", + }, + }, + }, + }, "other") + require.Error(t, e, errNoMultipleServicesWithAppHost) + require.False(t, r) +} + +func TestImportManagerHasServiceErrorAppHostMustTargetContainerApp(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + mockEnv := &mockenv.MockEnvManager{} + mockEnv.On("Save", mock.Anything, mock.Anything).Return(nil) + + manager := NewImportManager(&DotNetImporter{ + dotnetCli: dotnet.NewDotNetCli(mockContext.CommandRunner), + console: mockContext.Console, + lazyEnv: lazy.NewLazy(func() (*environment.Environment, error) { + return environment.NewWithValues("env", map[string]string{}), nil + }), + lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { + return mockEnv, nil + }), + hostCheck: make(map[string]hostCheckResult), + }) + + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "dotnet") && + slices.Contains(args.Args, "--getProperty:IsAspireHost") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.RunResult{ + Stdout: "true", + ExitCode: 0, + }, nil + }) + + // errors ** errNoMultipleServicesWithAppHost ** + r, e := manager.HasService(*mockContext.Context, &ProjectConfig{ + Path: "path", + Services: map[string]*ServiceConfig{ + "test": { + Name: "test", + Language: ServiceLanguageDotNet, + Host: StaticWebAppTarget, + RelativePath: "path", + Project: &ProjectConfig{ + Path: "path", + }, + }, + }, + }, "other") + require.Error(t, e, errAppHostMustTargetContainerApp) + require.False(t, r) +} + +func TestImportManagerProjectInfrastructureDefaults(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + mockEnv := &mockenv.MockEnvManager{} + mockEnv.On("Save", mock.Anything, mock.Anything).Return(nil) + + manager := NewImportManager(&DotNetImporter{ + dotnetCli: dotnet.NewDotNetCli(mockContext.CommandRunner), + console: mockContext.Console, + lazyEnv: lazy.NewLazy(func() (*environment.Environment, error) { + return environment.NewWithValues("env", map[string]string{}), nil + }), + lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { + return mockEnv, nil + }), + hostCheck: make(map[string]hostCheckResult), + }) + + // Get defaults and error b/c no infra found and no Aspire project + r, e := manager.ProjectInfrastructure(*mockContext.Context, &ProjectConfig{}) + require.Nil(t, r) + require.Error(t, e, "this project does not contain expected infrastructure") + + // adding infra folder to test defaults + expectedDefaultFolder := DefaultPath + err := os.Mkdir(expectedDefaultFolder, os.ModePerm) + require.NoError(t, err) + defer os.RemoveAll(expectedDefaultFolder) + + // error should keep happening b/c infra folder exists but module is not found + r, e = manager.ProjectInfrastructure(*mockContext.Context, &ProjectConfig{}) + require.Nil(t, r) + require.Error(t, e, "this project does not contain expected infrastructure") + + // Create the file + expectedDefaultModule := DefaultModule + path := filepath.Join(expectedDefaultFolder, expectedDefaultModule) + err = os.WriteFile(path, []byte(""), 0600) + require.NoError(t, err) + defer os.Remove(path) + + // infra result should be returned now that the default values are found + r, e = manager.ProjectInfrastructure(*mockContext.Context, &ProjectConfig{}) + require.NoError(t, e) + require.Equal(t, expectedDefaultFolder, r.Options.Path) + require.Equal(t, expectedDefaultModule, r.Options.Module) +} + +func TestImportManagerProjectInfrastructure(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + mockEnv := &mockenv.MockEnvManager{} + mockEnv.On("Save", mock.Anything, mock.Anything).Return(nil) + + manager := NewImportManager(&DotNetImporter{ + dotnetCli: dotnet.NewDotNetCli(mockContext.CommandRunner), + console: mockContext.Console, + lazyEnv: lazy.NewLazy(func() (*environment.Environment, error) { + return environment.NewWithValues("env", map[string]string{}), nil + }), + lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { + return mockEnv, nil + }), + hostCheck: make(map[string]hostCheckResult), + }) + + // Do not use defaults + expectedDefaultFolder := "customFolder" + expectedDefaultModule := "customModule" + + // adding infra folder to test defaults + err := os.Mkdir(expectedDefaultFolder, os.ModePerm) + require.NoError(t, err) + defer os.RemoveAll(expectedDefaultFolder) + + // Create the file + path := filepath.Join(expectedDefaultFolder, expectedDefaultModule) + err = os.WriteFile(path, []byte(""), 0600) + require.NoError(t, err) + defer os.Remove(path) + + r, e := manager.ProjectInfrastructure(*mockContext.Context, &ProjectConfig{ + Infra: provisioning.Options{ + Path: expectedDefaultFolder, + Module: expectedDefaultModule, + }, + }) + + require.NoError(t, e) + require.Equal(t, expectedDefaultFolder, r.Options.Path) + require.Equal(t, expectedDefaultModule, r.Options.Module) +} + +//go:embed testdata/aspire-escaping.json +var aspireEscapingManifest []byte + +func TestImportManagerProjectInfrastructureAspire(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + mockEnv := &mockenv.MockEnvManager{} + mockEnv.On("Save", mock.Anything, mock.Anything).Return(nil) + + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "dotnet") && + slices.Contains(args.Args, "--getProperty:IsAspireHost") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.RunResult{ + Stdout: "true", + ExitCode: 0, + }, nil + }) + + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "dotnet") && + slices.Contains(args.Args, "--publisher") && + slices.Contains(args.Args, "manifest") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + err := os.WriteFile(args.Args[6], aspireEscapingManifest, osutil.PermissionFile) + if err != nil { + return exec.RunResult{ + ExitCode: -1, + Stderr: err.Error(), + }, err + } + return exec.RunResult{}, nil + }) + + manager := NewImportManager(&DotNetImporter{ + dotnetCli: dotnet.NewDotNetCli(mockContext.CommandRunner), + console: mockContext.Console, + lazyEnv: lazy.NewLazy(func() (*environment.Environment, error) { + env := environment.NewWithValues("env", map[string]string{}) + // set the config to skip prompting + env.Config.Set("services.test.config.exposedServices", []interface{}{"test"}) + return env, nil + }), + lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { + return mockEnv, nil + }), + hostCheck: make(map[string]hostCheckResult), + cache: make(map[string]*apphost.Manifest), + }) + + // adding infra folder to test defaults + err := os.Mkdir(DefaultPath, os.ModePerm) + require.NoError(t, err) + defer os.RemoveAll(DefaultPath) + + // Simulating the scenario where a customer is using Aspire and has an infra folder with a module that is not the default + path := filepath.Join(DefaultPath, "customModule") + err = os.WriteFile(path, []byte(""), 0600) + require.NoError(t, err) + defer os.Remove(path) + + // Use an a dotnet project and use the mock to simulate an Aspire project + r, e := manager.ProjectInfrastructure(*mockContext.Context, &ProjectConfig{ + Services: map[string]*ServiceConfig{ + "test": { + Name: "test", + Language: ServiceLanguageDotNet, + Host: ContainerAppTarget, + RelativePath: "path", + Project: &ProjectConfig{ + Path: "path", + }, + }, + }, + }) + + require.NoError(t, e) + // dotnet_importer creates a temp path for the infrastructure. + // We can't figure the exact path, but it should contain the `azd-infra` label in it + require.Contains(t, r.Options.Path, "azd-infra") + require.Equal(t, DefaultModule, r.Options.Module) + require.Equal(t, r.cleanupDir, r.Options.Path) + require.NotNil(t, r.Inputs) +} diff --git a/cli/azd/pkg/project/project_manager.go b/cli/azd/pkg/project/project_manager.go index ba1da97d97f..69eec233f44 100644 --- a/cli/azd/pkg/project/project_manager.go +++ b/cli/azd/pkg/project/project_manager.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package project import ( diff --git a/cli/azd/pkg/project/testdata/aspire-escaping.json b/cli/azd/pkg/project/testdata/aspire-escaping.json new file mode 100644 index 00000000000..1ae19bd57fb --- /dev/null +++ b/cli/azd/pkg/project/testdata/aspire-escaping.json @@ -0,0 +1,32 @@ +{ + "resources": { + "pg": { + "type": "postgres.server.v0" + }, + "db": { + "type": "postgres.database.v0", + "parent": "pg" + }, + "api": { + "type": "project.v0", + "path": "..\\MyBlog.Api\\MyBlog.Api.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", + "ConnectionStrings__db": "{db.connectionString}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + } + } +} \ No newline at end of file From b1462606f933c6b840eaeaf3b0af36d7e5e9d2f3 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 30 Jan 2024 05:57:17 +0000 Subject: [PATCH 08/10] lint --- cli/azd/pkg/project/importer_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index 224a41708a8..2a2b477c1f8 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -295,7 +295,8 @@ func TestImportManagerProjectInfrastructureAspire(t *testing.T) { lazyEnv: lazy.NewLazy(func() (*environment.Environment, error) { env := environment.NewWithValues("env", map[string]string{}) // set the config to skip prompting - env.Config.Set("services.test.config.exposedServices", []interface{}{"test"}) + e := env.Config.Set("services.test.config.exposedServices", []interface{}{"test"}) + require.NoError(t, e) return env, nil }), lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { From 4b2e984d91993b850615037d2e18dd931e83c4b7 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 5 Feb 2024 21:31:13 -0800 Subject: [PATCH 09/10] Update cli/azd/pkg/project/importer.go Co-authored-by: Matt Ellis --- cli/azd/pkg/project/importer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index 1ea0407f954..c1db699710c 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -155,6 +155,7 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi projectConfig.Infra.Module) } ++// pathHasModule returns true if there is a file named "" or "" in path. func pathHasModule(path, module string) (bool, error) { files, err := os.ReadDir(path) if err != nil { From e2132a697e1ef9ab7820ae4ed4195df7d7e48219 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 6 Feb 2024 05:34:28 +0000 Subject: [PATCH 10/10] remove typo --- cli/azd/pkg/project/importer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index c1db699710c..daf877c28da 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -155,7 +155,7 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi projectConfig.Infra.Module) } -+// pathHasModule returns true if there is a file named "" or "" in path. +// pathHasModule returns true if there is a file named "" or "" in path. func pathHasModule(path, module string) (bool, error) { files, err := os.ReadDir(path) if err != nil {