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
1 change: 1 addition & 0 deletions cli/azd/cmd/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func (a *downAction) Run(ctx context.Context) (*actions.ActionResult, error) {
a.console.Message(ctx, "")
}

layer.Mode = provisioning.ModeDestroy
if err := a.provisionManager.Initialize(ctx, a.projectConfig.Path, layer); err != nil {
return nil, fmt.Errorf("initializing provisioning manager: %w", err)
}
Expand Down
4 changes: 3 additions & 1 deletion cli/azd/internal/vsrpc/environment_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ func (s *environmentService) DeleteEnvironmentAsync(
}
defer func() { _ = projectInfra.Cleanup() }()

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

Expand Down
4 changes: 2 additions & 2 deletions cli/azd/pkg/azapi/standard_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ func (ds *StandardDeployments) DeleteResourceGroupDeployment(
progress.SetProgress(DeleteDeploymentProgress{
Name: resourceGroupName,
Message: fmt.Sprintf("Failed resource group %s", output.WithHighLightFormat(resourceGroupName)),
State: DeleteResourceStateInProgress,
State: DeleteResourceStateFailed,
})

return err
Expand All @@ -518,7 +518,7 @@ func (ds *StandardDeployments) DeleteResourceGroupDeployment(
progress.SetProgress(DeleteDeploymentProgress{
Name: resourceGroupName,
Message: fmt.Sprintf("Deleted resource group %s", output.WithHighLightFormat(resourceGroupName)),
State: DeleteResourceStateInProgress,
State: DeleteResourceStateSucceeded,
})

return nil
Expand Down
99 changes: 76 additions & 23 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const (
bicepparamMode
)

const (
// apiVersion for checking if a resource group exists
apiVersionResourceGroupExistence = "2025-03-01"
)

// BicepProvider exposes infrastructure provisioning using Azure Bicep templates
type BicepProvider struct {
// Options that are available after Initialize()
Expand Down Expand Up @@ -114,9 +119,13 @@ func (p *BicepProvider) Initialize(ctx context.Context, projectPath string, opt
p.options = infraOptions
p.ignoreDeploymentState = infraOptions.IgnoreDeploymentState

p.console.ShowSpinner(ctx, "Initialize bicep provider", input.Step)
err = p.EnsureEnv(ctx)
p.console.StopSpinner(ctx, "", input.Step)
if opt.Mode == provisioning.ModeDeploy {
// For regular deployments, ensure the environment is in a provision-ready state
p.console.ShowSpinner(ctx, "Initialize bicep provider", input.Step)
err = p.EnsureEnv(ctx)
p.console.StopSpinner(ctx, "", input.Step)
}

return err
}

Expand Down Expand Up @@ -161,19 +170,55 @@ func (p *BicepProvider) EnsureEnv(ctx context.Context) error {
}

if scope == azure.DeploymentScopeResourceGroup {
if p.env.Getenv(environment.ResourceGroupEnvVarName) == "" {
// Prompt Resource Group supports creating a new resource group
// And prompts for a location as part of creating a new resource group
rgName, err := p.prompters.PromptResourceGroup(ctx, prompt.PromptResourceOptions{})
if err != nil {
return err
}
if err := p.ensureResourceGroup(ctx, p.env); err != nil {
return err
}
}

p.env.DotenvSet(environment.ResourceGroupEnvVarName, rgName)
if err := p.envManager.Save(ctx, p.env); err != nil {
return fmt.Errorf("saving resource group name: %w", err)
}
return nil
}

// ensureResourceGroup ensures that the resource group with AZURE_RESOURCE_GROUP key exists in the environment,
// prompting the user to create a resource group if it is unset or does not exist.
func (p *BicepProvider) ensureResourceGroup(ctx context.Context, env *environment.Environment) error {
promptAndSave := func(opt prompt.PromptResourceOptions) error {
rgName, err := p.prompters.PromptResourceGroup(ctx, opt)
if err != nil {
return err
}

p.env.DotenvSet(environment.ResourceGroupEnvVarName, rgName)
if err := p.envManager.Save(ctx, p.env); err != nil {
return fmt.Errorf("saving resource group name: %w", err)
}

return nil
}

resourceGroup := env.Getenv(environment.ResourceGroupEnvVarName)
if resourceGroup == "" {
return promptAndSave(prompt.PromptResourceOptions{})
}

resourceId := fmt.Sprintf(
"/subscriptions/%s/resourceGroups/%s",
p.env.GetSubscriptionId(),
resourceGroup)

resId, err := arm.ParseResourceID(resourceId)
if err != nil {
return fmt.Errorf("invalid '%s': %w", environment.ResourceGroupEnvVarName, err)
}

exists, err := p.resourceService.CheckExistenceByID(ctx, *resId, apiVersionResourceGroupExistence)
if err != nil {
return fmt.Errorf("checking if resource group exists: %w", err)
}

if !exists {
// Resource group no longer exists, prompt the user to create a new one.
// This handles the case where a resource group was deleted.
return promptAndSave(prompt.PromptResourceOptions{DefaultName: resourceGroup})
}

return nil
Expand Down Expand Up @@ -592,7 +637,7 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult,
if res != nil && res.ID != nil {
resId, err := arm.ParseResourceID(*res.ID)
if err == nil && resId.ResourceType.Type == arm.ResourceGroupResourceType.Type {
exists, err := p.resourceService.CheckExistenceByID(ctx, *resId, "2025-03-01")
exists, err := p.resourceService.CheckExistenceByID(ctx, *resId, apiVersionResourceGroupExistence)
if err == nil && !exists {
stateErr = fmt.Errorf(
"resource group %s no longer exists, invalidating deployment state", resId.ResourceGroupName)
Expand Down Expand Up @@ -831,6 +876,22 @@ func (p *BicepProvider) Destroy(
return nil, fmt.Errorf("creating template: %w", err)
}

targetScope, err := compileResult.Template.TargetScope()
if err != nil {
return nil, fmt.Errorf("computing deployment scope: %w", err)
}

switch targetScope {
case azure.DeploymentScopeResourceGroup:
if p.env.Getenv(environment.ResourceGroupEnvVarName) == "" {
return nil, azapi.ErrDeploymentNotFound
}
case azure.DeploymentScopeSubscription:
if p.env.Getenv(environment.SubscriptionIdEnvVarName) == "" || p.env.Getenv(environment.LocationEnvVarName) == "" {
return nil, azapi.ErrDeploymentNotFound
}
}

scope, err := p.scopeForTemplate(compileResult.Template)
if err != nil {
return nil, fmt.Errorf("computing deployment scope: %w", err)
Expand Down Expand Up @@ -959,14 +1020,6 @@ func (p *BicepProvider) Destroy(
))),
}

// Since we have deleted the resource group, add AZURE_RESOURCE_GROUP to the list of invalidated env vars
// so it will be removed from the .env file.
if _, ok := scope.(*infra.ResourceGroupScope); ok {
destroyResult.InvalidatedEnvKeys = append(
destroyResult.InvalidatedEnvKeys, environment.ResourceGroupEnvVarName,
)
}

return destroyResult, nil
}

Expand Down
19 changes: 16 additions & 3 deletions cli/azd/pkg/infra/provisioning/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,31 @@ const (
Test ProviderKind = "test"
)

type Mode string

const (
// Default mode for deploying or previewing the deployment.
ModeDeploy Mode = ""
// Mode for destroying the deployment.
ModeDestroy Mode = "destroy"
)

// Options for a provisioning provider.
type Options struct {
Provider ProviderKind `yaml:"provider,omitempty"`
Path string `yaml:"path,omitempty"`
Module string `yaml:"module,omitempty"`
Name string `yaml:"name,omitempty"`
DeploymentStacks map[string]any `yaml:"deploymentStacks,omitempty"`
// Not expected to be defined at azure.yaml
IgnoreDeploymentState bool `yaml:"-"`

// Provisioning options for each individually defined layer.
Layers []Options `yaml:"layers,omitempty"`

// Runtime options

// IgnoreDeploymentState when true, skips the deployment state check.
IgnoreDeploymentState bool `yaml:"-"`
// The mode in which the deployment is being run.
Mode Mode `yaml:"-"`
Comment thread
weikanglim marked this conversation as resolved.
}

// GetWithDefaults merges the provided infra options with the default provisioning options
Expand Down
2 changes: 0 additions & 2 deletions cli/azd/pkg/infra/provisioning/test/test_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,6 @@ func (p *TestProvider) Destroy(
ctx context.Context,
options provisioning.DestroyOptions,
) (*provisioning.DestroyResult, error) {
// TODO: progress, "Starting destroy"

destroyResult := provisioning.DestroyResult{
InvalidatedEnvKeys: []string{},
}
Expand Down
13 changes: 12 additions & 1 deletion cli/azd/pkg/infra/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ package infra

import (
"context"
"errors"
"fmt"
"net/http"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/azure/azure-dev/cli/azd/pkg/async"
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
Expand Down Expand Up @@ -215,7 +219,14 @@ func (s *ResourceGroupScope) ResourceGroupName() string {

// ListDeployments returns all the deployments in this resource group.
func (s *ResourceGroupScope) ListDeployments(ctx context.Context) ([]*azapi.ResourceDeployment, error) {
return s.deploymentService.ListResourceGroupDeployments(ctx, s.subscriptionId, s.resourceGroupName)
deployments, err := s.deploymentService.ListResourceGroupDeployments(ctx, s.subscriptionId, s.resourceGroupName)

var respErr *azcore.ResponseError
if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("%w: %w", ErrDeploymentsNotFound, respErr)
}

return deployments, err
}

// Deployment gets the deployment with the specified name.
Expand Down
17 changes: 16 additions & 1 deletion cli/azd/pkg/prompt/prompter.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ type Prompter interface {
msg string,
filter LocationFilterPredicate,
defaultLocation *string) (string, error)

// PromptResourceGroup prompts for an existing or optionally a new resource group.
// A location saved as AZURE_LOCATION is also prompted as part of creating a new resource group.
PromptResourceGroup(ctx context.Context, options PromptResourceOptions) (string, error)

// PromptResourceGroupFrom is like PromptResourceGroup, but it takes an existing subscription ID and location.
PromptResourceGroupFrom(
ctx context.Context, subscriptionId string, location string, options PromptResourceGroupFromOptions) (string, error)
}
Expand Down Expand Up @@ -123,10 +128,20 @@ func (p *DefaultPrompter) PromptLocation(
}

type PromptResourceOptions struct {
// DisableCreateNew disables the option to create a new resource group.
DisableCreateNew bool

// DefaultName is the default name to use when creating a new resource group.
// If not specified, the default name will be generated based on the environment name.
DefaultName string
}

func (p *DefaultPrompter) PromptResourceGroup(ctx context.Context, options PromptResourceOptions) (string, error) {
name := options.DefaultName
if name == "" {
name = fmt.Sprintf("rg-%s", p.env.Name())
}

return p.PromptResourceGroupFrom(
ctx,
p.env.GetSubscriptionId(),
Expand All @@ -135,7 +150,7 @@ func (p *DefaultPrompter) PromptResourceGroup(ctx context.Context, options Promp
Tags: map[string]string{
azure.TagKeyAzdEnvName: p.env.Name(),
},
DefaultName: fmt.Sprintf("rg-%s", p.env.Name()),
DefaultName: name,
DisableCreateNew: options.DisableCreateNew,
})
}
Expand Down
Loading
Loading