Skip to content
Open
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
53 changes: 44 additions & 9 deletions cli/azd/cmd/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ 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/ioc"
"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 @@ -66,9 +68,9 @@ func newDownCmd() *cobra.Command {
type downAction struct {
flags *downFlags
args []string
provisionManager *provisioning.Manager
serviceLocator ioc.ServiceLocator
importManager *project.ImportManager
env *environment.Environment
lazyEnv *lazy.Lazy[*environment.Environment]
envManager environment.Manager
console input.Console
projectConfig *project.ProjectConfig
Expand All @@ -78,8 +80,8 @@ type downAction struct {
func newDownAction(
args []string,
flags *downFlags,
provisionManager *provisioning.Manager,
env *environment.Environment,
serviceLocator ioc.ServiceLocator,
lazyEnv *lazy.Lazy[*environment.Environment],
envManager environment.Manager,
projectConfig *project.ProjectConfig,
console input.Console,
Expand All @@ -88,8 +90,8 @@ func newDownAction(
) actions.Action {
return &downAction{
flags: flags,
provisionManager: provisionManager,
env: env,
serviceLocator: serviceLocator,
lazyEnv: lazyEnv,
envManager: envManager,
console: console,
projectConfig: projectConfig,
Expand All @@ -108,6 +110,39 @@ func (a *downAction) Run(ctx context.Context) (*actions.ActionResult, error) {

startTime := time.Now()

// Check if there are any environments before proceeding
envList, err := a.envManager.List(ctx)
if err != nil {
return nil, fmt.Errorf("listing environments: %w", err)
}
if len(envList) == 0 {
return nil, errors.New("no environments found. Run \"azd init\" or \"azd env new\" to create one")
}
Comment on lines +113 to +120
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still getting prompted to enter env name on azd down:

Image


// Get the environment non-interactively (respects -e flag or default environment)
env, err := a.lazyEnv.GetValue()
if err != nil {
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

envManager.List() is only used to check for zero environments, but the subsequent lazyEnv.GetValue() can still fail with environment.ErrNameNotSpecified (e.g., when environments exist but no default is set and -e isn’t provided) and the error returned to the user will be unclear ("environment not specified"). Consider using the already-fetched envList to resolve the target environment: if -e is set, validate it exists and return a friendly not-found error; otherwise use the default from envList (or auto-pick when there is exactly one env) and if ambiguous, return an actionable message (e.g., suggest azd env select or azd down -e <name>).

Suggested change
if err != nil {
if err != nil {
if errors.Is(err, environment.ErrNameNotSpecified) {
// There are environments, but none has been selected or set as default.
// Provide an actionable message instead of a low-context error.
if len(envList) > 1 {
return nil, errors.New(
"no environment selected. Use \"azd env select\" to choose a default environment, or run \"azd down -e <name>\" to target a specific environment",
)
}
return nil, errors.New(
"no environment selected. Run \"azd env select\" to set a default environment, or \"azd down -e <name>\" to target a specific environment",
)
}

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The error path from lazyEnv.GetValue() is returned directly. When the user passes -e for a missing environment, the current error will likely be the low-level "'<name>': environment not found" without guidance. Consider mapping environment.ErrNotFound to a more actionable message (similar to env select / show), e.g., instructing to run azd env list, azd env new, or correct the -e value.

Suggested change
if err != nil {
if err != nil {
if errors.Is(err, environment.ErrNotFound) {
return nil, fmt.Errorf(
"environment not found. Run \"azd env list\" to see available environments, "+
"\"azd env new\" to create a new one, or specify a valid environment name with -e",
)
}

Copilot uses AI. Check for mistakes.
if errors.Is(err, environment.ErrNotFound) {
return nil, fmt.Errorf(
"environment not found. 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, errors.New(
"no environment selected. Use \"azd env select\" to set a default environment, " +
"or run \"azd down -e <name>\" to target a specific environment",
)
}
return nil, err
}

// Resolve provisioning manager after env check to avoid premature interactive prompts
var provisionManager *provisioning.Manager
if err := a.serviceLocator.Resolve(&provisionManager); err != nil {
return nil, fmt.Errorf("getting provisioning manager: %w", err)
}

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

layer.Mode = provisioning.ModeDestroy
if err := a.provisionManager.Initialize(ctx, a.projectConfig.Path, layer); err != nil {
if err := provisionManager.Initialize(ctx, a.projectConfig.Path, layer); err != nil {
return nil, fmt.Errorf("initializing provisioning manager: %w", err)
}

destroyOptions := provisioning.NewDestroyOptions(a.flags.forceDelete, a.flags.purgeDelete)
_, err := a.provisionManager.Destroy(ctx, destroyOptions)
_, err := provisionManager.Destroy(ctx, destroyOptions)
if errors.Is(err, inf.ErrDeploymentsNotFound) || errors.Is(err, inf.ErrDeploymentResourcesNotFound) {
a.console.MessageUxItem(ctx, &ux.DoneMessage{Message: "No Azure resources were found."})
} else if err != nil {
Expand All @@ -155,7 +190,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
134 changes: 134 additions & 0 deletions cli/azd/cmd/down_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"fmt"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"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/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"
)

// failServiceLocator is a test helper that fails the test if any IoC resolution is attempted.
type failServiceLocator struct {
t *testing.T
}

func (f *failServiceLocator) Resolve(instance any) error {
f.t.Fatal("serviceLocator.Resolve should not be called")
return nil
}

func (f *failServiceLocator) ResolveNamed(name string, instance any) error {
f.t.Fatal("serviceLocator.ResolveNamed should not be called")
return nil
}

func (f *failServiceLocator) Invoke(resolver any) error {
f.t.Fatal("serviceLocator.Invoke should not be called")
return nil
}

var _ ioc.ServiceLocator = (*failServiceLocator)(nil)

func newTestDownAction(
t *testing.T,
mockContext *mocks.MockContext,
envManager *mockenv.MockEnvManager,
lazyEnv *lazy.Lazy[*environment.Environment],
serviceLocator ioc.ServiceLocator,
) *downAction {
t.Helper()
if serviceLocator == nil {
serviceLocator = &failServiceLocator{t: t}
}
alphaManager := alpha.NewFeaturesManagerWithConfig(nil)
action := newDownAction(
[]string{},
&downFlags{},
serviceLocator,
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{}
envManager.On("List", mock.Anything).
Return([]*environment.Description{}, nil)

// lazyEnv and serviceLocator must NOT be called when no environments exist
lazyEnv := lazy.NewLazy(func() (*environment.Environment, error) {
t.Fatal("lazyEnv should not be evaluated when no environments exist")
return nil, nil
})

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

_, err := action.Run(*mockContext.Context)
require.Error(t, err)
require.Contains(t, err.Error(), "no environments found")

envManager.AssertExpectations(t)
}

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

envManager := &mockenv.MockEnvManager{}
envManager.On("List", mock.Anything).
Return([]*environment.Description{{Name: "some-env"}}, nil)

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

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

_, err := action.Run(*mockContext.Context)
require.Error(t, err)
require.Contains(t, err.Error(), "environment not found")
require.Contains(t, err.Error(), "azd env list")

envManager.AssertExpectations(t)
}

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

envManager := &mockenv.MockEnvManager{}
envManager.On("List", mock.Anything).
Return([]*environment.Description{{Name: "env1"}, {Name: "env2"}}, nil)

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

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

_, err := action.Run(*mockContext.Context)
require.Error(t, err)
require.Contains(t, err.Error(), "no environment selected")
require.Contains(t, err.Error(), "azd env select")

envManager.AssertExpectations(t)
}
Loading