Skip to content
Closed
29 changes: 25 additions & 4 deletions cli/azd/cmd/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
inf "github.com/azure/azure-dev/cli/azd/pkg/infra"
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
"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/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/project"
Expand Down Expand Up @@ -68,7 +69,7 @@ type downAction struct {
args []string
provisionManager *provisioning.Manager
importManager *project.ImportManager
env *environment.Environment
lazyEnv *lazy.Lazy[*environment.Environment]
envManager environment.Manager
console input.Console
projectConfig *project.ProjectConfig
Expand All @@ -79,7 +80,7 @@ func newDownAction(
args []string,
flags *downFlags,
provisionManager *provisioning.Manager,
env *environment.Environment,
lazyEnv *lazy.Lazy[*environment.Environment],
envManager environment.Manager,
projectConfig *project.ProjectConfig,
console input.Console,
Expand All @@ -89,7 +90,7 @@ func newDownAction(
return &downAction{
flags: flags,
provisionManager: provisionManager,
env: env,
lazyEnv: lazyEnv,
envManager: envManager,
console: console,
projectConfig: projectConfig,
Expand All @@ -108,6 +109,26 @@ func (a *downAction) Run(ctx context.Context) (*actions.ActionResult, error) {

startTime := time.Now()

// Get the environment non-interactively (respects -e flag or default environment)
env, err := a.lazyEnv.GetValue()
if err != nil {
if errors.Is(err, environment.ErrNotFound) {
return nil, &internal.ErrorWithSuggestion{
Err: errors.New("environment not found"),
Suggestion: "Run \"azd env list\" to see available environments, " +
"\"azd env new\" to create a new one, or specify a valid environment name with -e",
}
}
if errors.Is(err, environment.ErrNameNotSpecified) {
return nil, &internal.ErrorWithSuggestion{
Err: errors.New("no environment selected"),
Suggestion: "Run \"azd init\" or \"azd env new\" to create an environment, " +
"\"azd env select\" to set a default, or run \"azd down -e <name>\" to target a specific environment",
}
}
return nil, err
}

infra, err := a.importManager.ProjectInfrastructure(ctx, a.projectConfig)
if err != nil {
return nil, err
Expand Down Expand Up @@ -155,7 +176,7 @@ func (a *downAction) Run(ctx context.Context) (*actions.ActionResult, error) {
}

// Invalidate cache after successful down so azd show will refresh
if err := a.envManager.InvalidateEnvCache(ctx, a.env.Name()); err != nil {
if err := a.envManager.InvalidateEnvCache(ctx, env.Name()); err != nil {
log.Printf("warning: failed to invalidate state cache: %v", err)
}

Expand Down
152 changes: 152 additions & 0 deletions cli/azd/cmd/down_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"errors"
"fmt"
"testing"

"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/cloud"
"github.com/azure/azure-dev/cli/azd/pkg/config"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"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/project"
"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 newTestProvisionManager(
mockContext *mocks.MockContext,
lazyEnv *lazy.Lazy[*environment.Environment],
envManager environment.Manager,
) *provisioning.Manager {
alphaManager := alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig())
return provisioning.NewManager(
mockContext.Container,
func() (provisioning.ProviderKind, error) { return provisioning.Bicep, nil },
envManager,
lazyEnv,
mockContext.Console,
alphaManager,
nil,
cloud.AzurePublic(),
)
}

func newTestDownAction(
t *testing.T,
mockContext *mocks.MockContext,
envManager *mockenv.MockEnvManager,
lazyEnv *lazy.Lazy[*environment.Environment],
provisionManager *provisioning.Manager,
) *downAction {
t.Helper()
alphaManager := alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig())
action := newDownAction(
[]string{},
&downFlags{},
provisionManager,
lazyEnv,
envManager,
&project.ProjectConfig{},
mockContext.Console,
alphaManager,
project.NewImportManager(nil),
)
return action.(*downAction)
}

func Test_DownAction_NoEnvironments_ReturnsError(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())

envManager := &mockenv.MockEnvManager{}

// lazyEnv must NOT be called when no env exists and it returns ErrNameNotSpecified
lazyEnv := lazy.NewLazy(func() (*environment.Environment, error) {
return nil, environment.ErrNameNotSpecified
})
provisionManager := newTestProvisionManager(mockContext, lazyEnv, envManager)

action := newTestDownAction(t, mockContext, envManager, lazyEnv, provisionManager)

_, err := action.Run(*mockContext.Context)
require.Error(t, err)

var suggestionErr *internal.ErrorWithSuggestion
require.True(t, errors.As(err, &suggestionErr))
require.Contains(t, suggestionErr.Error(), "no environment selected")
require.Contains(t, suggestionErr.Suggestion, "azd env new")
}

func Test_DownAction_EnvironmentNotFound_ReturnsError(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())

envManager := &mockenv.MockEnvManager{}

// Simulate -e flag pointing to a missing environment
lazyEnv := lazy.NewLazy(func() (*environment.Environment, error) {
return nil, fmt.Errorf("'missing-env': %w", environment.ErrNotFound)
})
provisionManager := newTestProvisionManager(mockContext, lazyEnv, envManager)

action := newTestDownAction(t, mockContext, envManager, lazyEnv, provisionManager)

_, err := action.Run(*mockContext.Context)
require.Error(t, err)

var suggestionErr *internal.ErrorWithSuggestion
require.True(t, errors.As(err, &suggestionErr))
require.Contains(t, suggestionErr.Error(), "environment not found")
require.Contains(t, suggestionErr.Suggestion, "azd env list")
}

func Test_DownAction_NoDefaultEnvironment_ReturnsError(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())

envManager := &mockenv.MockEnvManager{}

// No -e flag and no default environment set
lazyEnv := lazy.NewLazy(func() (*environment.Environment, error) {
return nil, environment.ErrNameNotSpecified
})
provisionManager := newTestProvisionManager(mockContext, lazyEnv, envManager)

action := newTestDownAction(t, mockContext, envManager, lazyEnv, provisionManager)

_, err := action.Run(*mockContext.Context)
require.Error(t, err)

var suggestionErr *internal.ErrorWithSuggestion
require.True(t, errors.As(err, &suggestionErr))
require.Contains(t, suggestionErr.Error(), "no environment selected")
require.Contains(t, suggestionErr.Suggestion, "azd env select")
}

func Test_DownAction_EnvironmentExists_ProceedsToProvisioning(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())

envManager := &mockenv.MockEnvManager{}
envManager.On("InvalidateEnvCache", mock.Anything, mock.Anything).Return(nil)

env := environment.NewWithValues("test-env", nil)
lazyEnv := lazy.From(env)
provisionManager := newTestProvisionManager(mockContext, lazyEnv, envManager)

action := newTestDownAction(t, mockContext, envManager, lazyEnv, provisionManager)

_, err := action.Run(*mockContext.Context)
// The action must get past the env check and reach provisioning.
// It will fail on Initialize (no IaC provider in mock container), which is expected.
// The key assertion is: the error is NOT an env-check error.
require.Error(t, err)
var suggestionErr *internal.ErrorWithSuggestion
require.False(t, errors.As(err, &suggestionErr), "Expected a provisioning error, not an env-check error")
}
23 changes: 17 additions & 6 deletions cli/azd/cmd/middleware/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/ext"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/azure/azure-dev/cli/azd/pkg/lazy"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/project"
)

type HooksMiddleware struct {
envManager environment.Manager
env *environment.Environment
lazyEnv *lazy.Lazy[*environment.Environment]
Copy link
Contributor

@JeffreyCA JeffreyCA Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it regressed, now it just errors out with environment not found:
image

projectConfig *project.ProjectConfig
importManager *project.ImportManager
commandRunner exec.CommandRunner
Expand All @@ -32,7 +33,7 @@ type HooksMiddleware struct {
// Creates a new instance of the Hooks middleware
func NewHooksMiddleware(
envManager environment.Manager,
env *environment.Environment,
lazyEnv *lazy.Lazy[*environment.Environment],
projectConfig *project.ProjectConfig,
importManager *project.ImportManager,
commandRunner exec.CommandRunner,
Expand All @@ -42,7 +43,7 @@ func NewHooksMiddleware(
) Middleware {
return &HooksMiddleware{
envManager: envManager,
env: env,
lazyEnv: lazyEnv,
projectConfig: projectConfig,
importManager: importManager,
commandRunner: commandRunner,
Expand Down Expand Up @@ -81,6 +82,11 @@ func (m *HooksMiddleware) registerCommandHooks(
return next(ctx)
}

env, err := m.lazyEnv.GetValue()
if err != nil {
return nil, err
}

hooksManager := ext.NewHooksManager(m.projectConfig.Path, m.commandRunner)
hooksRunner := ext.NewHooksRunner(
hooksManager,
Expand All @@ -89,7 +95,7 @@ func (m *HooksMiddleware) registerCommandHooks(
m.console,
m.projectConfig.Path,
m.projectConfig.Hooks,
m.env,
env,
m.serviceLocator,
)

Expand All @@ -98,7 +104,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, func() error {
result, err := next(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -131,6 +137,11 @@ func (m *HooksMiddleware) registerServiceHooks(ctx context.Context) error {
continue
}

env, err := m.lazyEnv.GetValue()
if err != nil {
return err
}

serviceHooksManager := ext.NewHooksManager(service.Path(), m.commandRunner)
serviceHooksRunner := ext.NewHooksRunner(
serviceHooksManager,
Expand All @@ -139,7 +150,7 @@ func (m *HooksMiddleware) registerServiceHooks(ctx context.Context) error {
m.console,
service.Path(),
service.Hooks,
m.env,
env,
m.serviceLocator,
)

Expand Down
3 changes: 2 additions & 1 deletion cli/azd/cmd/middleware/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/ext"
"github.com/azure/azure-dev/cli/azd/pkg/lazy"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/azure/azure-dev/cli/azd/test/mocks/mockenv"
Expand Down Expand Up @@ -345,7 +346,7 @@ func runMiddleware(

middleware := NewHooksMiddleware(
envManager,
env,
lazy.From(env),
projectConfig,
project.NewImportManager(nil),
mockContext.CommandRunner,
Expand Down
Loading
Loading