From 4d59c6d2053638f3fd7a6d29b5d328d8081fb399 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 23 Aug 2024 19:13:56 +0000 Subject: [PATCH 1/7] service hooks --- cli/azd/cmd/hooks.go | 11 +++++++++++ cli/azd/cmd/middleware/hooks.go | 24 ++++++++++++++++++++++-- cli/azd/pkg/ext/models.go | 26 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/cli/azd/cmd/hooks.go b/cli/azd/cmd/hooks.go index b816db3d4f7..0df7a1cb3b5 100644 --- a/cli/azd/cmd/hooks.go +++ b/cli/azd/cmd/hooks.go @@ -146,6 +146,17 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro // Service level hooks for _, service := range stableServices { skip := hra.flags.service != "" && service.Name != hra.flags.service + hooksDefinedAtServicePath, err := ext.HooksFromServicePath(service.Path()) + if err != nil { + return nil, err + } + if service.Hooks != nil && hooksDefinedAtServicePath != nil { + return nil, fmt.Errorf("service %s has hooks defined in both azd.hooks.yaml and azure.yaml, "+ + "please remove one of them.", service.Name) + } + if service.Hooks == nil { + service.Hooks = hooksDefinedAtServicePath + } if err := hra.processHooks( ctx, diff --git a/cli/azd/cmd/middleware/hooks.go b/cli/azd/cmd/middleware/hooks.go index 660afe2655a..798ae4dcd1e 100644 --- a/cli/azd/cmd/middleware/hooks.go +++ b/cli/azd/cmd/middleware/hooks.go @@ -138,12 +138,32 @@ func (m *HooksMiddleware) registerServiceHooks( for _, service := range stableServices { serviceName := service.Name + + hooksDefinedAtServicePath, err := ext.HooksFromServicePath(service.Path()) + if err != nil { + return fmt.Errorf("failed getting hooks from service path, %w", err) + } + // If the service hasn't configured any hooks we can continue on. - if service.Hooks == nil || len(service.Hooks) == 0 { - log.Printf("service '%s' does not require any command hooks.\n", serviceName) + if (service.Hooks == nil || len(service.Hooks) == 0) && + (hooksDefinedAtServicePath == nil || len(hooksDefinedAtServicePath) == 0) { + log.Printf("service '%s' does not require any hooks.\n", serviceName) continue } + if (service.Hooks != nil && len(service.Hooks) > 0) && + (hooksDefinedAtServicePath != nil && len(hooksDefinedAtServicePath) > 0) { + return fmt.Errorf( + "service '%s' has hooks defined in both azd.hooks.yaml and azure.yaml configuration,"+ + " please remove one of them.", + serviceName) + } + + // If the service has hooks defined in azd.hooks.yaml but not in azure.yaml + if service.Hooks == nil || len(service.Hooks) == 0 { + service.Hooks = hooksDefinedAtServicePath + } + serviceHooksManager := ext.NewHooksManager(service.Path()) serviceHooksRunner := ext.NewHooksRunner( serviceHooksManager, diff --git a/cli/azd/pkg/ext/models.go b/cli/azd/pkg/ext/models.go index 1100c6a5ee6..b44c9e4ee71 100644 --- a/cli/azd/pkg/ext/models.go +++ b/cli/azd/pkg/ext/models.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "gopkg.in/yaml.v3" ) // The type of hooks. Supported values are 'pre' and 'post' @@ -218,3 +219,28 @@ func createTempScript(hookConfig *HookConfig) (string, error) { return file.Name(), nil } + +// HooksFromServicePath check if there is file named azd.hooks.yaml in the service path +// and return the hooks configuration. +func HooksFromServicePath(servicePath string) (map[string]*HookConfig, error) { + hooksPath := filepath.Join(servicePath, "azd.hooks.yaml") + if _, err := os.Stat(hooksPath); os.IsNotExist(err) { + hooksPath = filepath.Join(servicePath, "azd.hooks.yml") + if _, err := os.Stat(hooksPath); os.IsNotExist(err) { + return nil, nil + } + } + + hooksFile, err := os.ReadFile(hooksPath) + if err != nil { + return nil, fmt.Errorf("failed reading hooks from '%s', %w", hooksPath, err) + } + + // open hooksPath into a byte array and unmarshal it into a map[string]*ext.HookConfig + hooks := make(map[string]*HookConfig) + if err := yaml.Unmarshal(hooksFile, &hooks); err != nil { + return nil, fmt.Errorf("failed unmarshalling hooks from '%s', %w", hooksPath, err) + } + + return hooks, nil +} From 3c3b95473cf7f09c83257240577ac3d3ab8e0c06 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 23 Aug 2024 21:36:52 +0000 Subject: [PATCH 2/7] As part of merging azd operation and hooks, this PR allows users to define hooks outside of azure.yaml as individual files with the name azd.hooks.[yaml | yml]. The file can be placed in the /infra folder for project level hooks or inside service's folder for service level hooks. This will allow generators like Aspire to include hooks as part of the infrastructure code (w/o touching azure.yaml). Then, when running azd infra synth, the hooks are part of the infrastructure and users can delete the infra folder to go back to full Aspire in-memory generation w/o touching azure.yaml. After this PR, I am planning to move the azd operations to become a valid built-in hook and delete the alpha feature for azd operations and the entire feature in favor of just using the hooks strategy --- cli/azd/cmd/hooks.go | 19 ++++++++++++++++++- cli/azd/cmd/middleware/hooks.go | 29 +++++++++++++++++++++++------ cli/azd/pkg/ext/models.go | 4 ++-- cli/azd/pkg/project/project.go | 2 +- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/cli/azd/cmd/hooks.go b/cli/azd/cmd/hooks.go index 0df7a1cb3b5..b43f67a29b2 100644 --- a/cli/azd/cmd/hooks.go +++ b/cli/azd/cmd/hooks.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "path/filepath" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/internal" @@ -125,6 +126,22 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro } } + hooksDefinedAtInfraPath, err := ext.HooksFromFolderPath( + filepath.Join(hra.projectConfig.Path, hra.projectConfig.Infra.Path)) + if err != nil { + return nil, fmt.Errorf("failed getting hooks from infra path, %w", err) + } + if len(hooksDefinedAtInfraPath) > 0 && len(hra.projectConfig.Hooks) > 0 { + return nil, fmt.Errorf( + "project hooks defined in both %s and azure.yaml configuration,"+ + " please remove one of them", + filepath.Join(hra.projectConfig.Infra.Path, "azd.hooks.yaml"), + ) + } + if hra.projectConfig.Hooks == nil { + hra.projectConfig.Hooks = hooksDefinedAtInfraPath + } + // Project level hooks if err := hra.processHooks( ctx, @@ -146,7 +163,7 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro // Service level hooks for _, service := range stableServices { skip := hra.flags.service != "" && service.Name != hra.flags.service - hooksDefinedAtServicePath, err := ext.HooksFromServicePath(service.Path()) + hooksDefinedAtServicePath, err := ext.HooksFromFolderPath(service.Path()) if err != nil { return nil, err } diff --git a/cli/azd/cmd/middleware/hooks.go b/cli/azd/cmd/middleware/hooks.go index 50c0ac92e62..635c4348bbc 100644 --- a/cli/azd/cmd/middleware/hooks.go +++ b/cli/azd/cmd/middleware/hooks.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "path/filepath" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/pkg/environment" @@ -74,7 +75,14 @@ func (m *HooksMiddleware) registerCommandHooks( projectConfig *project.ProjectConfig, next NextFn, ) (*actions.ActionResult, error) { - if projectConfig.Hooks == nil || len(projectConfig.Hooks) == 0 { + + hooksDefinedAtInfraPath, err := ext.HooksFromFolderPath( + filepath.Join(projectConfig.Path, projectConfig.Infra.Path)) + if err != nil { + return nil, fmt.Errorf("failed getting hooks from infra path, %w", err) + } + + if len(projectConfig.Hooks) == 0 && len(hooksDefinedAtInfraPath) == 0 { log.Println( "azd project is not available or does not contain any command hooks, skipping command hook registrations.", ) @@ -86,6 +94,17 @@ func (m *HooksMiddleware) registerCommandHooks( return nil, fmt.Errorf("failed getting environment manager, %w", err) } + if len(hooksDefinedAtInfraPath) > 0 && len(projectConfig.Hooks) > 0 { + return nil, fmt.Errorf( + "project hooks defined in both %s and azure.yaml configuration,"+ + " please remove one of them", + filepath.Join(projectConfig.Infra.Path, "azd.hooks.yaml"), + ) + } + if projectConfig.Hooks == nil { + projectConfig.Hooks = hooksDefinedAtInfraPath + } + hooksManager := ext.NewHooksManager(projectConfig.Path) hooksRunner := ext.NewHooksRunner( hooksManager, @@ -139,20 +158,18 @@ func (m *HooksMiddleware) registerServiceHooks( for _, service := range stableServices { serviceName := service.Name - hooksDefinedAtServicePath, err := ext.HooksFromServicePath(service.Path()) + hooksDefinedAtServicePath, err := ext.HooksFromFolderPath(service.Path()) if err != nil { return fmt.Errorf("failed getting hooks from service path, %w", err) } // If the service hasn't configured any hooks we can continue on. - if (service.Hooks == nil || len(service.Hooks) == 0) && - (hooksDefinedAtServicePath == nil || len(hooksDefinedAtServicePath) == 0) { + if len(service.Hooks) == 0 && len(hooksDefinedAtServicePath) == 0 { log.Printf("service '%s' does not require any hooks.\n", serviceName) continue } - if (service.Hooks != nil && len(service.Hooks) > 0) && - (hooksDefinedAtServicePath != nil && len(hooksDefinedAtServicePath) > 0) { + if len(service.Hooks) > 0 && len(hooksDefinedAtServicePath) > 0 { return fmt.Errorf( "service '%s' has hooks defined in both azd.hooks.yaml and azure.yaml configuration,"+ " please remove one of them.", diff --git a/cli/azd/pkg/ext/models.go b/cli/azd/pkg/ext/models.go index b44c9e4ee71..7b3cc2aee43 100644 --- a/cli/azd/pkg/ext/models.go +++ b/cli/azd/pkg/ext/models.go @@ -220,9 +220,9 @@ func createTempScript(hookConfig *HookConfig) (string, error) { return file.Name(), nil } -// HooksFromServicePath check if there is file named azd.hooks.yaml in the service path +// HooksFromFolderPath check if there is file named azd.hooks.yaml in the service path // and return the hooks configuration. -func HooksFromServicePath(servicePath string) (map[string]*HookConfig, error) { +func HooksFromFolderPath(servicePath string) (map[string]*HookConfig, error) { hooksPath := filepath.Join(servicePath, "azd.hooks.yaml") if _, err := os.Stat(hooksPath); os.IsNotExist(err) { hooksPath = filepath.Join(servicePath, "azd.hooks.yml") diff --git a/cli/azd/pkg/project/project.go b/cli/azd/pkg/project/project.go index 854a8ec5419..db197c3742f 100644 --- a/cli/azd/pkg/project/project.go +++ b/cli/azd/pkg/project/project.go @@ -221,7 +221,7 @@ func Save(ctx context.Context, projectConfig *ProjectConfig, projectFilePath str return fmt.Errorf("saving project file: %w", err) } - projectConfig.Path = projectFilePath + projectConfig.Path = filepath.Dir(projectFilePath) return nil } From 9a561f5a0b17ce6425b0176fce0802c06dba13f3 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Sat, 24 Aug 2024 20:35:24 +0000 Subject: [PATCH 3/7] test new function --- cli/azd/pkg/ext/models_test.go | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 cli/azd/pkg/ext/models_test.go diff --git a/cli/azd/pkg/ext/models_test.go b/cli/azd/pkg/ext/models_test.go new file mode 100644 index 00000000000..e0a406d261e --- /dev/null +++ b/cli/azd/pkg/ext/models_test.go @@ -0,0 +1,110 @@ +package ext + +import ( + "os" + "path/filepath" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "github.com/stretchr/testify/require" +) + +func Test_HooksFromFolderPath(t *testing.T) { + t.Run("HooksFileExistsYaml", func(t *testing.T) { + tempDir := t.TempDir() + hooksPath := filepath.Join(tempDir, "azd.hooks.yaml") + hooksContent := []byte(` +pre-build: + shell: sh + run: ./pre-build.sh +post-build: + shell: pwsh + run: ./post-build.ps1 +`) + + err := os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory) + require.NoError(t, err) + + expectedHooks := map[string]*HookConfig{ + "pre-build": { + validated: false, + cwd: "", + Name: "", + Shell: ShellTypeBash, + Run: "./pre-build.sh", + ContinueOnError: false, + Interactive: false, + Windows: nil, + Posix: nil, + }, + "post-build": { + validated: false, + cwd: "", + Name: "", + Shell: ShellTypePowershell, + Run: "./post-build.ps1", + ContinueOnError: false, + Interactive: false, + Windows: nil, + Posix: nil, + }, + } + + hooks, err := HooksFromFolderPath(tempDir) + require.NoError(t, err) + require.Equal(t, expectedHooks, hooks) + }) + + t.Run("HooksFileExistsYml", func(t *testing.T) { + tempDir := t.TempDir() + hooksPath := filepath.Join(tempDir, "azd.hooks.yml") + hooksContent := []byte(` +pre-build: + shell: sh + run: ./pre-build.sh +post-build: + shell: pwsh + run: ./post-build.ps1 +`) + + err := os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory) + require.NoError(t, err) + + expectedHooks := map[string]*HookConfig{ + "pre-build": { + validated: false, + cwd: "", + Name: "", + Shell: ShellTypeBash, + Run: "./pre-build.sh", + ContinueOnError: false, + Interactive: false, + Windows: nil, + Posix: nil, + }, + "post-build": { + validated: false, + cwd: "", + Name: "", + Shell: ShellTypePowershell, + Run: "./post-build.ps1", + ContinueOnError: false, + Interactive: false, + Windows: nil, + Posix: nil, + }, + } + + hooks, err := HooksFromFolderPath(tempDir) + require.NoError(t, err) + require.Equal(t, expectedHooks, hooks) + }) + + t.Run("NoHooksFile", func(t *testing.T) { + tempDir := t.TempDir() + hooks, err := HooksFromFolderPath(tempDir) + require.NoError(t, err) + var expectedHooks map[string]*HookConfig + require.Equal(t, expectedHooks, hooks) + }) +} From a65c3b43925438e4654049e370fc01a3be2c3e8a Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 26 Aug 2024 21:53:14 +0000 Subject: [PATCH 4/7] lint --- cli/azd/cmd/middleware/hooks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/cmd/middleware/hooks.go b/cli/azd/cmd/middleware/hooks.go index 635c4348bbc..be328e61360 100644 --- a/cli/azd/cmd/middleware/hooks.go +++ b/cli/azd/cmd/middleware/hooks.go @@ -177,7 +177,7 @@ func (m *HooksMiddleware) registerServiceHooks( } // If the service has hooks defined in azd.hooks.yaml but not in azure.yaml - if service.Hooks == nil || len(service.Hooks) == 0 { + if len(service.Hooks) == 0 { service.Hooks = hooksDefinedAtServicePath } From af3da51ad70452a4b6140c955b0248e68fa8d36f Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 29 Aug 2024 19:24:48 +0000 Subject: [PATCH 5/7] wait here --- cli/azd/pkg/ext/models.go | 4 ++-- cli/azd/pkg/project/project.go | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/ext/models.go b/cli/azd/pkg/ext/models.go index 7b3cc2aee43..cf2901d02ab 100644 --- a/cli/azd/pkg/ext/models.go +++ b/cli/azd/pkg/ext/models.go @@ -222,7 +222,7 @@ func createTempScript(hookConfig *HookConfig) (string, error) { // HooksFromFolderPath check if there is file named azd.hooks.yaml in the service path // and return the hooks configuration. -func HooksFromFolderPath(servicePath string) (map[string]*HookConfig, error) { +func HooksFromFolderPath(servicePath string) (map[string][]*HookConfig, error) { hooksPath := filepath.Join(servicePath, "azd.hooks.yaml") if _, err := os.Stat(hooksPath); os.IsNotExist(err) { hooksPath = filepath.Join(servicePath, "azd.hooks.yml") @@ -237,7 +237,7 @@ func HooksFromFolderPath(servicePath string) (map[string]*HookConfig, error) { } // open hooksPath into a byte array and unmarshal it into a map[string]*ext.HookConfig - hooks := make(map[string]*HookConfig) + hooks := make(map[string][]*HookConfig) if err := yaml.Unmarshal(hooksFile, &hooks); err != nil { return nil, fmt.Errorf("failed unmarshalling hooks from '%s', %w", hooksPath, err) } diff --git a/cli/azd/pkg/project/project.go b/cli/azd/pkg/project/project.go index 176ef6647a2..96b742230c4 100644 --- a/cli/azd/pkg/project/project.go +++ b/cli/azd/pkg/project/project.go @@ -130,6 +130,22 @@ func Load(ctx context.Context, projectFilePath string) (*ProjectConfig, error) { return nil, fmt.Errorf("parsing project file: %w", err) } + // complement the project config with azd.hooks files if they exist + hooksDefinedAtInfraPath, err := ext.HooksFromFolderPath(filepath.Join(projectConfig.Path, projectConfig.Infra.Path)) + if err != nil { + return nil, fmt.Errorf("failed getting hooks from infra path, %w", err) + } + if len(hooksDefinedAtInfraPath) > 0 && len(projectConfig.Hooks) > 0 { + return nil, fmt.Errorf( + "project hooks defined in both %s and azure.yaml configuration,"+ + " please remove one of them", + filepath.Join(projectConfig.Infra.Path, "azd.hooks.yaml"), + ) + } + if projectConfig.Hooks == nil { + projectConfig.Hooks = hooksDefinedAtInfraPath + } + if projectConfig.Metadata != nil && projectConfig.Metadata.Template != "" { template := strings.Split(projectConfig.Metadata.Template, "@") if len(template) == 1 { // no version specifier, just the template ID From a292068200223598884c8d4d26d8a2a0672a8a39 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 30 Aug 2024 01:19:51 +0000 Subject: [PATCH 6/7] User project to parse hooks from external files --- cli/azd/cmd/hooks.go | 28 ---- cli/azd/cmd/middleware/hooks.go | 43 +----- cli/azd/pkg/ext/models.go | 26 ---- cli/azd/pkg/ext/models_test.go | 110 -------------- cli/azd/pkg/project/project.go | 44 +++++- cli/azd/pkg/project/project_test.go | 215 ++++++++++++++++++++++++++++ 6 files changed, 260 insertions(+), 206 deletions(-) delete mode 100644 cli/azd/pkg/ext/models_test.go diff --git a/cli/azd/cmd/hooks.go b/cli/azd/cmd/hooks.go index 810f64cc893..09ef37c6b2b 100644 --- a/cli/azd/cmd/hooks.go +++ b/cli/azd/cmd/hooks.go @@ -3,7 +3,6 @@ package cmd import ( "context" "fmt" - "path/filepath" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/internal" @@ -126,22 +125,6 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro } } - hooksDefinedAtInfraPath, err := ext.HooksFromFolderPath( - filepath.Join(hra.projectConfig.Path, hra.projectConfig.Infra.Path)) - if err != nil { - return nil, fmt.Errorf("failed getting hooks from infra path, %w", err) - } - if len(hooksDefinedAtInfraPath) > 0 && len(hra.projectConfig.Hooks) > 0 { - return nil, fmt.Errorf( - "project hooks defined in both %s and azure.yaml configuration,"+ - " please remove one of them", - filepath.Join(hra.projectConfig.Infra.Path, "azd.hooks.yaml"), - ) - } - if hra.projectConfig.Hooks == nil { - hra.projectConfig.Hooks = hooksDefinedAtInfraPath - } - // Project level hooks projectHooks := hra.projectConfig.Hooks[hookName] @@ -166,17 +149,6 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro for _, service := range stableServices { serviceHooks := service.Hooks[hookName] skip := hra.flags.service != "" && service.Name != hra.flags.service - hooksDefinedAtServicePath, err := ext.HooksFromFolderPath(service.Path()) - if err != nil { - return nil, err - } - if service.Hooks != nil && hooksDefinedAtServicePath != nil { - return nil, fmt.Errorf("service %s has hooks defined in both azd.hooks.yaml and azure.yaml, "+ - "please remove one of them.", service.Name) - } - if service.Hooks == nil { - service.Hooks = hooksDefinedAtServicePath - } if err := hra.processHooks( ctx, diff --git a/cli/azd/cmd/middleware/hooks.go b/cli/azd/cmd/middleware/hooks.go index be328e61360..aca4011f5de 100644 --- a/cli/azd/cmd/middleware/hooks.go +++ b/cli/azd/cmd/middleware/hooks.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "path/filepath" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/pkg/environment" @@ -75,14 +74,7 @@ func (m *HooksMiddleware) registerCommandHooks( projectConfig *project.ProjectConfig, next NextFn, ) (*actions.ActionResult, error) { - - hooksDefinedAtInfraPath, err := ext.HooksFromFolderPath( - filepath.Join(projectConfig.Path, projectConfig.Infra.Path)) - if err != nil { - return nil, fmt.Errorf("failed getting hooks from infra path, %w", err) - } - - if len(projectConfig.Hooks) == 0 && len(hooksDefinedAtInfraPath) == 0 { + if len(projectConfig.Hooks) == 0 { log.Println( "azd project is not available or does not contain any command hooks, skipping command hook registrations.", ) @@ -94,17 +86,6 @@ func (m *HooksMiddleware) registerCommandHooks( return nil, fmt.Errorf("failed getting environment manager, %w", err) } - if len(hooksDefinedAtInfraPath) > 0 && len(projectConfig.Hooks) > 0 { - return nil, fmt.Errorf( - "project hooks defined in both %s and azure.yaml configuration,"+ - " please remove one of them", - filepath.Join(projectConfig.Infra.Path, "azd.hooks.yaml"), - ) - } - if projectConfig.Hooks == nil { - projectConfig.Hooks = hooksDefinedAtInfraPath - } - hooksManager := ext.NewHooksManager(projectConfig.Path) hooksRunner := ext.NewHooksRunner( hooksManager, @@ -157,28 +138,10 @@ func (m *HooksMiddleware) registerServiceHooks( for _, service := range stableServices { serviceName := service.Name - - hooksDefinedAtServicePath, err := ext.HooksFromFolderPath(service.Path()) - if err != nil { - return fmt.Errorf("failed getting hooks from service path, %w", err) - } - // If the service hasn't configured any hooks we can continue on. - if len(service.Hooks) == 0 && len(hooksDefinedAtServicePath) == 0 { - log.Printf("service '%s' does not require any hooks.\n", serviceName) - continue - } - - if len(service.Hooks) > 0 && len(hooksDefinedAtServicePath) > 0 { - return fmt.Errorf( - "service '%s' has hooks defined in both azd.hooks.yaml and azure.yaml configuration,"+ - " please remove one of them.", - serviceName) - } - - // If the service has hooks defined in azd.hooks.yaml but not in azure.yaml if len(service.Hooks) == 0 { - service.Hooks = hooksDefinedAtServicePath + log.Printf("service '%s' does not require any command hooks.\n", serviceName) + continue } serviceHooksManager := ext.NewHooksManager(service.Path()) diff --git a/cli/azd/pkg/ext/models.go b/cli/azd/pkg/ext/models.go index cf2901d02ab..1100c6a5ee6 100644 --- a/cli/azd/pkg/ext/models.go +++ b/cli/azd/pkg/ext/models.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/azure/azure-dev/cli/azd/pkg/osutil" - "gopkg.in/yaml.v3" ) // The type of hooks. Supported values are 'pre' and 'post' @@ -219,28 +218,3 @@ func createTempScript(hookConfig *HookConfig) (string, error) { return file.Name(), nil } - -// HooksFromFolderPath check if there is file named azd.hooks.yaml in the service path -// and return the hooks configuration. -func HooksFromFolderPath(servicePath string) (map[string][]*HookConfig, error) { - hooksPath := filepath.Join(servicePath, "azd.hooks.yaml") - if _, err := os.Stat(hooksPath); os.IsNotExist(err) { - hooksPath = filepath.Join(servicePath, "azd.hooks.yml") - if _, err := os.Stat(hooksPath); os.IsNotExist(err) { - return nil, nil - } - } - - hooksFile, err := os.ReadFile(hooksPath) - if err != nil { - return nil, fmt.Errorf("failed reading hooks from '%s', %w", hooksPath, err) - } - - // open hooksPath into a byte array and unmarshal it into a map[string]*ext.HookConfig - hooks := make(map[string][]*HookConfig) - if err := yaml.Unmarshal(hooksFile, &hooks); err != nil { - return nil, fmt.Errorf("failed unmarshalling hooks from '%s', %w", hooksPath, err) - } - - return hooks, nil -} diff --git a/cli/azd/pkg/ext/models_test.go b/cli/azd/pkg/ext/models_test.go deleted file mode 100644 index e0a406d261e..00000000000 --- a/cli/azd/pkg/ext/models_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package ext - -import ( - "os" - "path/filepath" - "testing" - - "github.com/azure/azure-dev/cli/azd/pkg/osutil" - "github.com/stretchr/testify/require" -) - -func Test_HooksFromFolderPath(t *testing.T) { - t.Run("HooksFileExistsYaml", func(t *testing.T) { - tempDir := t.TempDir() - hooksPath := filepath.Join(tempDir, "azd.hooks.yaml") - hooksContent := []byte(` -pre-build: - shell: sh - run: ./pre-build.sh -post-build: - shell: pwsh - run: ./post-build.ps1 -`) - - err := os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory) - require.NoError(t, err) - - expectedHooks := map[string]*HookConfig{ - "pre-build": { - validated: false, - cwd: "", - Name: "", - Shell: ShellTypeBash, - Run: "./pre-build.sh", - ContinueOnError: false, - Interactive: false, - Windows: nil, - Posix: nil, - }, - "post-build": { - validated: false, - cwd: "", - Name: "", - Shell: ShellTypePowershell, - Run: "./post-build.ps1", - ContinueOnError: false, - Interactive: false, - Windows: nil, - Posix: nil, - }, - } - - hooks, err := HooksFromFolderPath(tempDir) - require.NoError(t, err) - require.Equal(t, expectedHooks, hooks) - }) - - t.Run("HooksFileExistsYml", func(t *testing.T) { - tempDir := t.TempDir() - hooksPath := filepath.Join(tempDir, "azd.hooks.yml") - hooksContent := []byte(` -pre-build: - shell: sh - run: ./pre-build.sh -post-build: - shell: pwsh - run: ./post-build.ps1 -`) - - err := os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory) - require.NoError(t, err) - - expectedHooks := map[string]*HookConfig{ - "pre-build": { - validated: false, - cwd: "", - Name: "", - Shell: ShellTypeBash, - Run: "./pre-build.sh", - ContinueOnError: false, - Interactive: false, - Windows: nil, - Posix: nil, - }, - "post-build": { - validated: false, - cwd: "", - Name: "", - Shell: ShellTypePowershell, - Run: "./post-build.ps1", - ContinueOnError: false, - Interactive: false, - Windows: nil, - Posix: nil, - }, - } - - hooks, err := HooksFromFolderPath(tempDir) - require.NoError(t, err) - require.Equal(t, expectedHooks, hooks) - }) - - t.Run("NoHooksFile", func(t *testing.T) { - tempDir := t.TempDir() - hooks, err := HooksFromFolderPath(tempDir) - require.NoError(t, err) - var expectedHooks map[string]*HookConfig - require.Equal(t, expectedHooks, hooks) - }) -} diff --git a/cli/azd/pkg/project/project.go b/cli/azd/pkg/project/project.go index 96b742230c4..2d35f1f7771 100644 --- a/cli/azd/pkg/project/project.go +++ b/cli/azd/pkg/project/project.go @@ -130,8 +130,10 @@ func Load(ctx context.Context, projectFilePath string) (*ProjectConfig, error) { return nil, fmt.Errorf("parsing project file: %w", err) } + projectConfig.Path = filepath.Dir(projectFilePath) + // complement the project config with azd.hooks files if they exist - hooksDefinedAtInfraPath, err := ext.HooksFromFolderPath(filepath.Join(projectConfig.Path, projectConfig.Infra.Path)) + hooksDefinedAtInfraPath, err := hooksFromFolderPath(filepath.Join(projectConfig.Path, projectConfig.Infra.Path)) if err != nil { return nil, fmt.Errorf("failed getting hooks from infra path, %w", err) } @@ -169,6 +171,20 @@ func Load(ctx context.Context, projectFilePath string) (*ProjectConfig, error) { for _, svcConfig := range projectConfig.Services { hosts[i] = string(svcConfig.Host) languages[i] = string(svcConfig.Language) + + // complement service level hooks + hooksDefinedAtServicePath, err := hooksFromFolderPath(svcConfig.RelativePath) + if err != nil { + return nil, err + } + if svcConfig.Hooks != nil && hooksDefinedAtServicePath != nil { + return nil, fmt.Errorf("service %s has hooks defined in both azd.hooks.yaml and azure.yaml, "+ + "please remove one of them.", svcConfig.Name) + } + if svcConfig.Hooks == nil { + svcConfig.Hooks = hooksDefinedAtServicePath + } + i++ } @@ -179,7 +195,6 @@ func Load(ctx context.Context, projectFilePath string) (*ProjectConfig, error) { tracing.SetUsageAttributes(fields.ProjectServiceHostsKey.StringSlice(hosts)) } - projectConfig.Path = filepath.Dir(projectFilePath) return projectConfig, nil } @@ -241,3 +256,28 @@ func Save(ctx context.Context, projectConfig *ProjectConfig, projectFilePath str return nil } + +// hooksFromFolderPath check if there is file named azd.hooks.yaml in the service path +// and return the hooks configuration. +func hooksFromFolderPath(servicePath string) (HooksConfig, error) { + hooksPath := filepath.Join(servicePath, "azd.hooks.yaml") + if _, err := os.Stat(hooksPath); os.IsNotExist(err) { + hooksPath = filepath.Join(servicePath, "azd.hooks.yml") + if _, err := os.Stat(hooksPath); os.IsNotExist(err) { + return nil, nil + } + } + + hooksFile, err := os.ReadFile(hooksPath) + if err != nil { + return nil, fmt.Errorf("failed reading hooks from '%s', %w", hooksPath, err) + } + + // open hooksPath into a byte array and unmarshal it into a map[string]*ext.HookConfig + hooks := make(HooksConfig) + if err := yaml.Unmarshal(hooksFile, &hooks); err != nil { + return nil, fmt.Errorf("failed unmarshalling hooks from '%s', %w", hooksPath, err) + } + + return hooks, nil +} diff --git a/cli/azd/pkg/project/project_test.go b/cli/azd/pkg/project/project_test.go index 7b0bda37427..ca2ff02c2f8 100644 --- a/cli/azd/pkg/project/project_test.go +++ b/cli/azd/pkg/project/project_test.go @@ -5,6 +5,8 @@ package project import ( "context" + "os" + "path/filepath" "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" @@ -12,6 +14,8 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/azure" "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/ext" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/test/mocks" "github.com/azure/azure-dev/cli/azd/test/mocks/mockarmresources" "github.com/azure/azure-dev/cli/azd/test/mocks/mockazcli" @@ -311,3 +315,214 @@ func TestMinimalYaml(t *testing.T) { }) } } + +func Test_HooksFromFolderPath(t *testing.T) { + t.Run("ProjectInfraHooks", func(t *testing.T) { + prj := &ProjectConfig{ + Name: "minimal", + Services: map[string]*ServiceConfig{}, + } + contents, err := yaml.Marshal(prj) + require.NoError(t, err) + + tempDir := t.TempDir() + + azureYamlPath := filepath.Join(tempDir, "azure.yaml") + err = os.WriteFile(azureYamlPath, contents, osutil.PermissionDirectory) + require.NoError(t, err) + + infraPath := filepath.Join(tempDir, "infra") + err = os.Mkdir(infraPath, osutil.PermissionDirectory) + require.NoError(t, err) + + hooksPath := filepath.Join(infraPath, "azd.hooks.yaml") + hooksContent := []byte(` +pre-build: + shell: sh + run: ./pre-build.sh +post-build: + shell: pwsh + run: ./post-build.ps1 +`) + + err = os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory) + require.NoError(t, err) + + expectedHooks := HooksConfig{ + "pre-build": {{ + Name: "", + Shell: ext.ShellTypeBash, + Run: "./pre-build.sh", + ContinueOnError: false, + Interactive: false, + Windows: nil, + Posix: nil, + }}, + "post-build": {{ + Name: "", + Shell: ext.ShellTypePowershell, + Run: "./post-build.ps1", + ContinueOnError: false, + Interactive: false, + Windows: nil, + Posix: nil, + }, + }} + + project, err := Load(context.Background(), azureYamlPath) + require.NoError(t, err) + require.Equal(t, expectedHooks, project.Hooks) + }) + + t.Run("ErrorDoubleDefintionHooks", func(t *testing.T) { + prj := &ProjectConfig{ + Name: "minimal", + Services: map[string]*ServiceConfig{}, + Hooks: HooksConfig{ + "prebuild": {{ + Run: "./pre-build.sh", + }}, + }, + } + contents, err := yaml.Marshal(prj) + require.NoError(t, err) + + tempDir := t.TempDir() + + azureYamlPath := filepath.Join(tempDir, "azure.yaml") + err = os.WriteFile(azureYamlPath, contents, osutil.PermissionDirectory) + require.NoError(t, err) + + infraPath := filepath.Join(tempDir, "infra") + err = os.Mkdir(infraPath, osutil.PermissionDirectory) + require.NoError(t, err) + + hooksPath := filepath.Join(infraPath, "azd.hooks.yaml") + hooksContent := []byte(` +pre-build: + shell: sh + run: ./pre-build.sh +post-build: + shell: pwsh + run: ./post-build.ps1 +`) + + err = os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory) + require.NoError(t, err) + + project, err := Load(context.Background(), azureYamlPath) + require.Error(t, err) + var expectedProject *ProjectConfig + require.Equal(t, expectedProject, project) + }) + + t.Run("ServiceInfraHooks", func(t *testing.T) { + tempDir := t.TempDir() + + prj := &ProjectConfig{ + Name: "minimal", + Services: map[string]*ServiceConfig{ + "api": { + Name: "api", + Host: AppServiceTarget, + RelativePath: filepath.Join(tempDir, "api"), + }, + }, + } + contents, err := yaml.Marshal(prj) + require.NoError(t, err) + + azureYamlPath := filepath.Join(tempDir, "azure.yaml") + err = os.WriteFile(azureYamlPath, contents, osutil.PermissionDirectory) + require.NoError(t, err) + + servicePath := filepath.Join(tempDir, "api") + err = os.Mkdir(servicePath, osutil.PermissionDirectory) + require.NoError(t, err) + + hooksPath := filepath.Join(servicePath, "azd.hooks.yaml") + hooksContent := []byte(` +pre-build: + shell: sh + run: ./pre-build.sh +post-build: + shell: pwsh + run: ./post-build.ps1 +`) + + err = os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory) + require.NoError(t, err) + + expectedHooks := HooksConfig{ + "pre-build": {{ + Name: "", + Shell: ext.ShellTypeBash, + Run: "./pre-build.sh", + ContinueOnError: false, + Interactive: false, + Windows: nil, + Posix: nil, + }}, + "post-build": {{ + Name: "", + Shell: ext.ShellTypePowershell, + Run: "./post-build.ps1", + ContinueOnError: false, + Interactive: false, + Windows: nil, + Posix: nil, + }, + }} + + project, err := Load(context.Background(), azureYamlPath) + require.NoError(t, err) + require.Equal(t, expectedHooks, project.Services["api"].Hooks) + }) + + t.Run("ErrorDoubleDefintionServiceHooks", func(t *testing.T) { + tempDir := t.TempDir() + prj := &ProjectConfig{ + Name: "minimal", + Services: map[string]*ServiceConfig{ + "api": { + Name: "api", + Host: AppServiceTarget, + Hooks: HooksConfig{ + "prebuild": {{ + Run: "./pre-build.sh", + }}, + }, + RelativePath: filepath.Join(tempDir, "api"), + }, + }, + } + contents, err := yaml.Marshal(prj) + require.NoError(t, err) + + azureYamlPath := filepath.Join(tempDir, "azure.yaml") + err = os.WriteFile(azureYamlPath, contents, osutil.PermissionDirectory) + require.NoError(t, err) + + servicePath := filepath.Join(tempDir, "api") + err = os.Mkdir(servicePath, osutil.PermissionDirectory) + require.NoError(t, err) + + hooksPath := filepath.Join(servicePath, "azd.hooks.yaml") + hooksContent := []byte(` +pre-build: + shell: sh + run: ./pre-build.sh +post-build: + shell: pwsh + run: ./post-build.ps1 +`) + + err = os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory) + require.NoError(t, err) + + project, err := Load(context.Background(), azureYamlPath) + require.Error(t, err) + var expectedProject *ProjectConfig + require.Equal(t, expectedProject, project) + }) +} From 1178156dd8c37cfb95425b0d78f88f0d92204966 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 30 Aug 2024 04:29:22 +0000 Subject: [PATCH 7/7] ignore all stat errors as a way to handle Aspire projects --- cli/azd/pkg/project/project.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/project/project.go b/cli/azd/pkg/project/project.go index 2d35f1f7771..01811909136 100644 --- a/cli/azd/pkg/project/project.go +++ b/cli/azd/pkg/project/project.go @@ -261,9 +261,17 @@ func Save(ctx context.Context, projectConfig *ProjectConfig, projectFilePath str // and return the hooks configuration. func hooksFromFolderPath(servicePath string) (HooksConfig, error) { hooksPath := filepath.Join(servicePath, "azd.hooks.yaml") - if _, err := os.Stat(hooksPath); os.IsNotExist(err) { + + // due to projects depending on a ProjectImporter like Aspire, we need to ignore all type of errors related to + // the path is either not found, not accessible or any other error that might occur. + // In case of Aspire, the servicePath points out to the C# project, and not to the directory. + // We could handle Aspire Project here but that's not the purpose of this function. + // The right thing might be to use the ProjectImporter and get the list of services from Aspire and check for hooks at + // each path, but hooks for Aspire services are not even supported on azure.yaml. + if _, err := os.Stat(hooksPath); err != nil { hooksPath = filepath.Join(servicePath, "azd.hooks.yml") - if _, err := os.Stat(hooksPath); os.IsNotExist(err) { + if _, err := os.Stat(hooksPath); err != nil { + log.Println("error trying to read hooks file for service in path: ", servicePath, ". Error:", err) return nil, nil } }