Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions cli/azd/cmd/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func newHooksRunAction(
type hookContextType string

const (
hookContextProject hookContextType = "command"
hookContextProject hookContextType = "project"
hookContextLayer hookContextType = "layer"
hookContextService hookContextType = "service"
)
Expand Down Expand Up @@ -301,7 +301,7 @@ func (hra *hooksRunAction) processHooks(

hra.console.Message(ctx, output.WithBold("%s hook %d/%d:", contextType, idx+1, len(hooks)))

err := hra.execHook(ctx, cwd, hookType, commandName, hook)
err := hra.execHook(ctx, cwd, hookType, commandName, hook, contextType)
if err != nil {
return fmt.Errorf("failed running hook %s, %w", hookName, err)
}
Expand All @@ -322,11 +322,12 @@ func (hra *hooksRunAction) processHooks(
func (hra *hooksRunAction) execHook(
ctx context.Context,
cwd string,
hookType ext.HookType,
ht ext.HookType,
commandName string,
hook *ext.HookConfig,
contextType hookContextType,
) error {
hookName := string(hookType) + commandName
hookName := string(ht) + commandName

hooksMap := map[string][]*ext.HookConfig{
hookName: {hook},
Expand All @@ -338,12 +339,20 @@ func (hra *hooksRunAction) execHook(
hooksRunner := ext.NewHooksRunner(
hooksManager, hra.commandRunner, hra.envManager, hra.console, cwd, hooksMap, hra.env, hra.serviceLocator)

hookType := "project"
switch contextType {
case hookContextLayer:
hookType = "layer"
case hookContextService:
hookType = "service"
}

// Always run in interactive mode for 'azd hooks run', to help with testing/debugging
runOptions := &tools.ExecutionContext{
Interactive: new(true),
}

err := hooksRunner.RunHooks(ctx, hookType, runOptions, commandName)
err := hooksRunner.RunHooks(ctx, ht, hookType, runOptions, commandName)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/cmd/middleware/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (m *HooksMiddleware) registerCommandHooks(
commandNames := []string{m.options.CommandPath}
commandNames = append(commandNames, m.options.Aliases...)

err := hooksRunner.Invoke(ctx, commandNames, func() error {
err := hooksRunner.Invoke(ctx, commandNames, "project", func() error {
result, err := next(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -198,7 +198,7 @@ func (m *HooksMiddleware) createServiceEventHandler(
hooksRunner *ext.HooksRunner,
) ext.EventHandlerFn[project.ServiceLifecycleEventArgs] {
return func(ctx context.Context, eventArgs project.ServiceLifecycleEventArgs) error {
return hooksRunner.RunHooks(ctx, hookType, nil, hookName)
return hooksRunner.RunHooks(ctx, hookType, "service", nil, hookName)
}
}

Expand Down
5 changes: 4 additions & 1 deletion cli/azd/internal/cmd/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,10 @@ func (p *ProvisionAction) runLayerProvisionWithHooks(

p.validateAndWarnLayerHooks(ctx, hooksManager, layer.Hooks)

if err := hooksRunner.Invoke(ctx, []string{string(project.ProjectEventProvision)}, actionFn); err != nil {
err := hooksRunner.Invoke(
ctx, []string{string(project.ProjectEventProvision)}, "layer", actionFn,
)
if err != nil {
Comment thread
wbreza marked this conversation as resolved.
if layer.Name == "" {
return err
}
Expand Down
6 changes: 6 additions & 0 deletions cli/azd/internal/tracing/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ const (
PreflightValidationEvent = "validation.preflight"
)

// Hook execution events.
const (
// HooksExecEvent tracks the execution of a lifecycle hook.
HooksExecEvent = "hooks.exec"
)

// AKS service target events.
const (
// AksPostprovisionSkipEvent tracks when the AKS postprovision hook
Expand Down
6 changes: 6 additions & 0 deletions cli/azd/internal/tracing/fields/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,12 @@ var (
Classification: SystemMetadata,
Purpose: FeatureInsight,
}
// The executor kind used to run the hook (e.g., "sh", "pwsh", "python", "js", "ts", "dotnet").
HooksKindKey = AttributeKey{
Key: attribute.Key("hooks.kind"),
Classification: SystemMetadata,
Purpose: FeatureInsight,
}
)

// Pipeline command related fields
Expand Down
69 changes: 54 additions & 15 deletions cli/azd/pkg/ext/hooks_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
"os"
"strings"

"github.com/azure/azure-dev/cli/azd/internal/tracing"
"github.com/azure/azure-dev/cli/azd/internal/tracing/events"
"github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/errorhandler"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
Expand All @@ -18,6 +21,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/keyvault"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"go.opentelemetry.io/otel/codes"
)

// HooksRunner enables support to invoke lifecycle hooks before & after
Expand All @@ -33,8 +37,8 @@ type HooksRunner struct {
serviceLocator ioc.ServiceLocator
}

// NewHooks creates a new instance of CommandHooks
// When `cwd` is empty defaults to current shell working directory
// NewHooksRunner creates a new instance of HooksRunner.
// When `cwd` is empty defaults to current shell working directory.
func NewHooksRunner(
hooksManager *HooksManager,
commandRunner exec.CommandRunner,
Expand Down Expand Up @@ -66,9 +70,12 @@ func NewHooksRunner(
}
}

// Invokes an action run runs any registered pre or post script hooks for the specified command.
func (h *HooksRunner) Invoke(ctx context.Context, commands []string, actionFn InvokeFn) error {
err := h.RunHooks(ctx, HookTypePre, nil, commands...)
// Invoke runs any registered pre and post script hooks around the specified action.
// The hookType parameter identifies the hook scope for telemetry (project, layer, or service).
func (h *HooksRunner) Invoke(
ctx context.Context, commands []string, hookType string, actionFn InvokeFn,
) error {
err := h.RunHooks(ctx, HookTypePre, hookType, nil, commands...)
if err != nil {
return fmt.Errorf("failed running pre hooks: %w", err)
}
Expand All @@ -78,22 +85,24 @@ func (h *HooksRunner) Invoke(ctx context.Context, commands []string, actionFn In
return err
}

err = h.RunHooks(ctx, HookTypePost, nil, commands...)
err = h.RunHooks(ctx, HookTypePost, hookType, nil, commands...)
if err != nil {
return fmt.Errorf("failed running post hooks: %w", err)
}

return nil
}

// Invokes any registered script hooks for the specified hook type and command.
// RunHooks invokes any registered script hooks for the specified hook type and command.
// The hookType parameter identifies the hook scope for telemetry (project, layer, or service).
func (h *HooksRunner) RunHooks(
ctx context.Context,
hookType HookType,
ht HookType,
hookType string,
options *tools.ExecutionContext,
commands ...string,
) error {
hooks, err := h.hooksManager.GetByParams(h.hooks, hookType, commands...)
hooks, err := h.hooksManager.GetByParams(h.hooks, ht, commands...)
if err != nil {
return fmt.Errorf("failed running scripts for hooks '%s', %w", strings.Join(commands, ","), err)
}
Expand All @@ -103,7 +112,7 @@ func (h *HooksRunner) RunHooks(
return fmt.Errorf("reloading environment before running hook: %w", err)
}

err := h.execHook(ctx, hookConfig, options)
err := h.execHook(ctx, hookConfig, hookType, options)
if err != nil {
return err
}
Expand All @@ -117,8 +126,24 @@ func (h *HooksRunner) RunHooks(
}

func (h *HooksRunner) execHook(
ctx context.Context, hookConfig *HookConfig, options *tools.ExecutionContext,
) error {
ctx context.Context, hookConfig *HookConfig, hookType string, options *tools.ExecutionContext,
) (hookErr error) {
// Create a child span for this hook execution so each hook
// gets its own correlated set of telemetry attributes.
ctx, span := tracing.Start(ctx, events.HooksExecEvent)
statusCode := ""
defer func() {
if hookErr != nil {
if statusCode == "" {
statusCode = "hook.failed"
}
span.SetStatus(codes.Error, statusCode)
} else {
span.SetStatus(codes.Ok, "")
}
span.End()
}()

if options == nil {
options = &tools.ExecutionContext{}
}
Expand All @@ -143,16 +168,27 @@ func (h *HooksRunner) execHook(
return nil
})
if err != nil {
statusCode = "hook.secrets_failed"
return err
}
}

// Set name and type on span — known before validation.
span.SetAttributes(
fields.HooksNameKey.String(hookConfig.Name),
fields.HooksTypeKey.String(hookType),
)

// validate() resolves the hook's kind, path, shell type,
// and computes resolvedDir / resolvedScriptPath.
if err := hookConfig.validate(); err != nil {
statusCode = "hook.validation_failed"
return err
}

span.SetAttributes(
fields.HooksKindKey.String(string(hookConfig.Kind)))

// Use pre-resolved paths from validate().
cwd := hookConfig.resolvedDir
if cwd == "" {
Expand Down Expand Up @@ -192,6 +228,7 @@ func (h *HooksRunner) execHook(
// Resolve executor via IoC — hooks runner has NO knowledge of executor internals.
var executor tools.HookExecutor
if err := h.serviceLocator.ResolveNamed(string(hookConfig.Kind), &executor); err != nil {
statusCode = "hook.executor_not_found"
return &errorhandler.ErrorWithSuggestion{
Err: fmt.Errorf(
"no executor for kind '%s': %w",
Expand Down Expand Up @@ -230,6 +267,7 @@ func (h *HooksRunner) execHook(
)

if err := executor.Prepare(ctx, scriptPath, execCtx); err != nil {
statusCode = "hook.prepare_failed"
return fmt.Errorf("preparing hook '%s': %w", hookConfig.Name, err)
}

Expand All @@ -246,11 +284,12 @@ func (h *HooksRunner) execHook(

res, err := executor.Execute(ctx, scriptPath, execCtx)
if err != nil {
hookErr := h.handleHookError(
execErr := h.handleHookError(
ctx, hookConfig, res, scriptPath, err,
)
if hookErr != nil {
return hookErr
if execErr != nil {
statusCode = "hook.execution_failed"
return execErr
}
}

Expand Down
Loading
Loading