Skip to content

Commit 87586f8

Browse files
authored
Support azd hooks out of azure.yaml, Part I of merging azd operation to hooks (#4244)
* service hooks * 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
1 parent a49be25 commit 87586f8

File tree

2 files changed

+183
-2
lines changed

2 files changed

+183
-2
lines changed

cli/azd/pkg/project/project.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ func Parse(ctx context.Context, yamlContent string) (*ProjectConfig, error) {
7777
projectConfig.Infra.Path = "infra"
7878
}
7979

80+
if projectConfig.Infra.Module == "" {
81+
projectConfig.Infra.Module = DefaultModule
82+
}
83+
8084
if strings.Contains(projectConfig.Infra.Path, "\\") && !strings.Contains(projectConfig.Infra.Path, "/") {
8185
projectConfig.Infra.Path = strings.ReplaceAll(projectConfig.Infra.Path, "\\", "/")
8286
}
@@ -154,6 +158,31 @@ func Load(ctx context.Context, projectFilePath string) (*ProjectConfig, error) {
154158
return nil, fmt.Errorf("parsing project file: %w", err)
155159
}
156160

161+
projectConfig.Path = filepath.Dir(projectFilePath)
162+
163+
// complement the project config with hooks defined in the infra path using the syntax `<moduleName>.hooks.yaml`
164+
// for example `main.hooks.yaml`
165+
hooksDefinedAtInfraPath, err := hooksFromInfraModule(
166+
filepath.Join(projectConfig.Path, projectConfig.Infra.Path),
167+
projectConfig.Infra.Module)
168+
if err != nil {
169+
return nil, fmt.Errorf("failed getting hooks from infra path, %w", err)
170+
}
171+
// Merge the hooks defined at the infra path with the hooks defined in the project configuration
172+
if len(hooksDefinedAtInfraPath) > 0 && projectConfig.Hooks == nil {
173+
projectConfig.Hooks = make(map[string][]*ext.HookConfig)
174+
}
175+
for hookName, externalHookList := range hooksDefinedAtInfraPath {
176+
if hookListFromAzureYaml, hookExists := projectConfig.Hooks[hookName]; hookExists {
177+
mergedHooks := make([]*ext.HookConfig, 0, len(hookListFromAzureYaml)+len(externalHookList))
178+
mergedHooks = append(mergedHooks, hookListFromAzureYaml...)
179+
mergedHooks = append(mergedHooks, externalHookList...)
180+
projectConfig.Hooks[hookName] = mergedHooks
181+
continue
182+
}
183+
projectConfig.Hooks[hookName] = externalHookList
184+
}
185+
157186
if projectConfig.Metadata != nil && projectConfig.Metadata.Template != "" {
158187
template := strings.Split(projectConfig.Metadata.Template, "@")
159188
if len(template) == 1 { // no version specifier, just the template ID
@@ -187,7 +216,6 @@ func Load(ctx context.Context, projectFilePath string) (*ProjectConfig, error) {
187216
tracing.SetUsageAttributes(fields.ProjectServiceHostsKey.StringSlice(hosts))
188217
}
189218

190-
projectConfig.Path = filepath.Dir(projectFilePath)
191219
return projectConfig, nil
192220
}
193221

@@ -271,7 +299,28 @@ func Save(ctx context.Context, projectConfig *ProjectConfig, projectFilePath str
271299
return fmt.Errorf("saving project file: %w", err)
272300
}
273301

274-
projectConfig.Path = projectFilePath
302+
projectConfig.Path = filepath.Dir(projectFilePath)
275303

276304
return nil
277305
}
306+
307+
// hooksFromInfraModule check if there is file named azd.hooks.yaml in the service path
308+
// and return the hooks configuration.
309+
func hooksFromInfraModule(infraPath, moduleName string) (HooksConfig, error) {
310+
hooksPath := filepath.Join(infraPath, moduleName+".hooks.yaml")
311+
if _, err := os.Stat(hooksPath); os.IsNotExist(err) {
312+
return nil, nil
313+
}
314+
hooksFile, err := os.ReadFile(hooksPath)
315+
if err != nil {
316+
return nil, fmt.Errorf("failed reading hooks from '%s', %w", hooksPath, err)
317+
}
318+
319+
// open hooksPath into a byte array and unmarshal it into a map[string]*ext.HookConfig
320+
hooks := make(HooksConfig)
321+
if err := yaml.Unmarshal(hooksFile, &hooks); err != nil {
322+
return nil, fmt.Errorf("failed unmarshalling hooks from '%s', %w", hooksPath, err)
323+
}
324+
325+
return hooks, nil
326+
}

cli/azd/pkg/project/project_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package project
55

66
import (
77
"context"
8+
"os"
89
"path/filepath"
910
"testing"
1011

@@ -13,7 +14,9 @@ import (
1314
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
1415
"github.com/azure/azure-dev/cli/azd/pkg/azure"
1516
"github.com/azure/azure-dev/cli/azd/pkg/environment"
17+
"github.com/azure/azure-dev/cli/azd/pkg/ext"
1618
"github.com/azure/azure-dev/cli/azd/pkg/infra"
19+
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
1720
"github.com/azure/azure-dev/cli/azd/test/mocks"
1821
"github.com/azure/azure-dev/cli/azd/test/mocks/mockarmresources"
1922
"github.com/azure/azure-dev/cli/azd/test/mocks/mockazcli"
@@ -342,3 +345,132 @@ services:
342345
assert.Equal(t, filepath.FromSlash("src/api"), projectConfig.Services["api"].RelativePath)
343346
assert.Equal(t, filepath.FromSlash("bin/api"), projectConfig.Services["api"].OutputPath)
344347
}
348+
349+
func Test_HooksFromFolderPath(t *testing.T) {
350+
t.Run("ProjectInfraHooks", func(t *testing.T) {
351+
prj := &ProjectConfig{
352+
Name: "minimal",
353+
Services: map[string]*ServiceConfig{},
354+
}
355+
contents, err := yaml.Marshal(prj)
356+
require.NoError(t, err)
357+
358+
tempDir := t.TempDir()
359+
360+
azureYamlPath := filepath.Join(tempDir, "azure.yaml")
361+
err = os.WriteFile(azureYamlPath, contents, osutil.PermissionDirectory)
362+
require.NoError(t, err)
363+
364+
infraPath := filepath.Join(tempDir, "infra")
365+
err = os.Mkdir(infraPath, osutil.PermissionDirectory)
366+
require.NoError(t, err)
367+
368+
hooksPath := filepath.Join(infraPath, "main.hooks.yaml")
369+
hooksContent := []byte(`
370+
prebuild:
371+
shell: sh
372+
run: ./pre-build.sh
373+
postbuild:
374+
shell: pwsh
375+
run: ./post-build.ps1
376+
`)
377+
378+
err = os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory)
379+
require.NoError(t, err)
380+
381+
expectedHooks := HooksConfig{
382+
"prebuild": {{
383+
Name: "",
384+
Shell: ext.ShellTypeBash,
385+
Run: "./pre-build.sh",
386+
ContinueOnError: false,
387+
Interactive: false,
388+
Windows: nil,
389+
Posix: nil,
390+
}},
391+
"postbuild": {{
392+
Name: "",
393+
Shell: ext.ShellTypePowershell,
394+
Run: "./post-build.ps1",
395+
ContinueOnError: false,
396+
Interactive: false,
397+
Windows: nil,
398+
Posix: nil,
399+
},
400+
}}
401+
402+
project, err := Load(context.Background(), azureYamlPath)
403+
require.NoError(t, err)
404+
require.Equal(t, expectedHooks, project.Hooks)
405+
})
406+
407+
t.Run("DoubleDefintionHooks", func(t *testing.T) {
408+
prj := &ProjectConfig{
409+
Name: "minimal",
410+
Services: map[string]*ServiceConfig{},
411+
Hooks: HooksConfig{
412+
"prebuild": {{
413+
Shell: ext.ShellTypeBash,
414+
Run: "./pre-build.sh",
415+
}},
416+
},
417+
}
418+
contents, err := yaml.Marshal(prj)
419+
require.NoError(t, err)
420+
421+
tempDir := t.TempDir()
422+
423+
azureYamlPath := filepath.Join(tempDir, "azure.yaml")
424+
err = os.WriteFile(azureYamlPath, contents, osutil.PermissionDirectory)
425+
require.NoError(t, err)
426+
427+
infraPath := filepath.Join(tempDir, "infra")
428+
err = os.Mkdir(infraPath, osutil.PermissionDirectory)
429+
require.NoError(t, err)
430+
431+
hooksPath := filepath.Join(infraPath, "main.hooks.yaml")
432+
hooksContent := []byte(`
433+
prebuild:
434+
shell: sh
435+
run: ./pre-build-external.sh
436+
postbuild:
437+
shell: pwsh
438+
run: ./post-build.ps1
439+
`)
440+
441+
err = os.WriteFile(hooksPath, hooksContent, osutil.PermissionDirectory)
442+
require.NoError(t, err)
443+
444+
project, err := Load(context.Background(), azureYamlPath)
445+
require.NoError(t, err)
446+
expectedHooks := HooksConfig{
447+
"prebuild": {{
448+
Name: "",
449+
Shell: ext.ShellTypeBash,
450+
Run: "./pre-build.sh",
451+
ContinueOnError: false,
452+
Interactive: false,
453+
Windows: nil,
454+
Posix: nil,
455+
}, {
456+
Name: "",
457+
Shell: ext.ShellTypeBash,
458+
Run: "./pre-build-external.sh",
459+
ContinueOnError: false,
460+
Interactive: false,
461+
Windows: nil,
462+
Posix: nil,
463+
}},
464+
"postbuild": {{
465+
Name: "",
466+
Shell: ext.ShellTypePowershell,
467+
Run: "./post-build.ps1",
468+
ContinueOnError: false,
469+
Interactive: false,
470+
Windows: nil,
471+
Posix: nil,
472+
}},
473+
}
474+
require.Equal(t, expectedHooks, project.Hooks)
475+
})
476+
}

0 commit comments

Comments
 (0)