Skip to content

Commit

Permalink
fix provision state after down (#2837)
Browse files Browse the repository at this point in the history
* fix by adding empty deployment on down
  • Loading branch information
vhvb1989 authored Oct 6, 2023
1 parent 188e1a8 commit 1cbed85
Show file tree
Hide file tree
Showing 13 changed files with 2,751 additions and 3,860 deletions.
8 changes: 2 additions & 6 deletions cli/azd/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# Release History

## 1.5.0-beta.1 (Unreleased)

### Features Added

### Breaking Changes
## 1.4.1 (2023-10-06)

### Bugs Fixed

### Other Changes
- [[2837]](https://github.com/Azure/azure-dev/pull/2837) `azd down` does not clear provision state.

## 1.4.0 (2023-10-05)

Expand Down
75 changes: 63 additions & 12 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,32 +363,36 @@ func (p *BicepProvider) plan(ctx context.Context) (*deploymentDetails, error) {
return nil, err
}

var target infra.Deployment
target, err := p.deploymentScope(deploymentScope)
if err != nil {
return nil, err
}

return &deploymentDetails{
CompiledBicep: compileResult,
Target: target,
}, nil
}

func (p *BicepProvider) deploymentScope(deploymentScope azure.DeploymentScope) (infra.Deployment, error) {
if deploymentScope == azure.DeploymentScopeSubscription {
target = infra.NewSubscriptionDeployment(
return infra.NewSubscriptionDeployment(
p.deploymentsService,
p.deploymentOperations,
p.env.GetLocation(),
p.env.GetSubscriptionId(),
deploymentNameForEnv(p.env.GetEnvName(), p.clock),
)
), nil
} else if deploymentScope == azure.DeploymentScopeResourceGroup {
target = infra.NewResourceGroupDeployment(
return infra.NewResourceGroupDeployment(
p.deploymentsService,
p.deploymentOperations,
p.env.GetSubscriptionId(),
p.env.Getenv(environment.ResourceGroupEnvVarName),
deploymentNameForEnv(p.env.GetEnvName(), p.clock),
)
} else {
return nil, fmt.Errorf("unsupported scope: %s", deploymentScope)
), nil
}

return &deploymentDetails{
CompiledBicep: compileResult,
Target: target,
}, nil
return nil, fmt.Errorf("unsupported scope: %s", deploymentScope)
}

// cArmDeploymentNameLengthMax is the maximum length of the name of a deployment in ARM.
Expand Down Expand Up @@ -746,6 +750,24 @@ func (p *BicepProvider) inferScopeFromEnv(ctx context.Context) (infra.Scope, err
}
}

const cEmptySubDeployTemplate = `{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"variables": {},
"resources": [],
"outputs": {}
}`

const cEmptyResourceGroupDeployTemplate = `{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"variables": {},
"resources": [],
"outputs": {}
}`

// Destroys the specified deployment by deleting all azure resources, resource groups & deployments that are referenced.
func (p *BicepProvider) Destroy(ctx context.Context, options DestroyOptions) (*DestroyResult, error) {
modulePath := p.modulePath()
Expand All @@ -760,6 +782,15 @@ func (p *BicepProvider) Destroy(ctx context.Context, options DestroyOptions) (*D
return nil, fmt.Errorf("computing deployment scope: %w", err)
}

targetScope, err := compileResult.Template.TargetScope()
if err != nil {
return nil, err
}
deployScope, err := p.deploymentScope(targetScope)
if err != nil {
return nil, err
}

// TODO: Report progress, "Fetching resource groups"
deployments, err := p.findCompletedDeployments(ctx, p.env.GetEnvName(), scope, "")
if err != nil {
Expand Down Expand Up @@ -882,6 +913,26 @@ func (p *BicepProvider) Destroy(ctx context.Context, options DestroyOptions) (*D
)
}

var emptyTemplate json.RawMessage
if targetScope == azure.DeploymentScopeSubscription {
emptyTemplate = []byte(cEmptySubDeployTemplate)
} else {
emptyTemplate = []byte(cEmptyResourceGroupDeployTemplate)
}

// create empty deployment to void provision state
// We want to keep the deployment history, that's why it's not just deleted
if _, err := p.deployModule(ctx,
deployScope,
emptyTemplate,
azure.ArmParameters{},
map[string]*string{
azure.TagKeyAzdEnvName: to.Ptr(p.env.GetEnvName()),
"azd-deploy-reason": to.Ptr("down"),
}); err != nil {
log.Println("failed creating new empty deployment after destroy")
}

return destroyResult, nil
}

Expand Down
5 changes: 5 additions & 0 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,11 @@ func prepareDestroyMocks(mockContext *mocks.MockContext) {
response.Header.Add("location", mockPollingUrl)
return response, err
})

mockContext.HttpClient.When(func(request *http.Request) bool {
return request.Method == http.MethodPut &&
strings.Contains(request.URL.Path, "/subscriptions/SUBSCRIPTION_ID/providers/Microsoft.Resources/deployments/")
}).RespondFn(httpRespondFn)
}

func getKeyVaultMock(mockContext *mocks.MockContext, keyVaultString string, name string, location string) {
Expand Down
57 changes: 53 additions & 4 deletions cli/azd/test/functional/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func Test_CLI_InfraCreateAndDelete(t *testing.T) {
require.NoError(t, err)
}

func Test_CLI_ProvisionCache(t *testing.T) {
func Test_CLI_ProvisionState(t *testing.T) {
t.Setenv("AZURE_RECORD_MODE", "live")
ctx, cancel := newTestContext(t)
defer cancel()
Expand All @@ -204,11 +204,12 @@ func Test_CLI_ProvisionCache(t *testing.T) {
_, err = cli.RunCommandWithStdIn(ctx, stdinForInit(envName), "init")
require.NoError(t, err)

_, err = cli.RunCommandWithStdIn(ctx, stdinForProvision(), "provision")
require.NoError(t, err)

expectedOutputContains := "There are no changes to provision for your application."

initial, err := cli.RunCommandWithStdIn(ctx, stdinForProvision(), "provision")
require.NoError(t, err)
require.NotContains(t, initial.Stdout, expectedOutputContains)

// Second provision should use cache
secondProvisionOutput, err := cli.RunCommandWithStdIn(ctx, stdinForProvision(), "provision")
require.NoError(t, err)
Expand All @@ -234,6 +235,54 @@ func Test_CLI_ProvisionCache(t *testing.T) {
require.NoError(t, err)
}

func Test_CLI_ProvisionStateWithDown(t *testing.T) {
t.Setenv("AZURE_RECORD_MODE", "live")
ctx, cancel := newTestContext(t)
defer cancel()

dir := tempDirWithDiagnostics(t)
t.Logf("DIR: %s", dir)

session := recording.Start(t)

envName := randomOrStoredEnvName(session)
t.Logf("AZURE_ENV_NAME: %s", envName)

cli := azdcli.NewCLI(t, azdcli.WithSession(session))
cli.WorkingDirectory = dir
cli.Env = append(cli.Env, os.Environ()...)
cli.Env = append(cli.Env, "AZURE_LOCATION=eastus2")

err := copySample(dir, "storage")
require.NoError(t, err, "failed expanding sample")

_, err = cli.RunCommandWithStdIn(ctx, stdinForInit(envName), "init")
require.NoError(t, err)

expectedOutputContains := "There are no changes to provision for your application."

initial, err := cli.RunCommandWithStdIn(ctx, stdinForProvision(), "provision")
require.NoError(t, err)
require.NotContains(t, initial.Stdout, expectedOutputContains)

// Second provision should use cache
secondProvisionOutput, err := cli.RunCommandWithStdIn(ctx, stdinForProvision(), "provision")
require.NoError(t, err)
require.Contains(t, secondProvisionOutput.Stdout, expectedOutputContains)

// down to delete resources
_, err = cli.RunCommandWithStdIn(ctx, "", "down", "--force", "--purge")
require.NoError(t, err)

// use flag to force provision
reProvisionAfterDown, err := cli.RunCommandWithStdIn(ctx, stdinForProvision(), "provision")
require.NoError(t, err)
require.NotContains(t, reProvisionAfterDown.Stdout, expectedOutputContains)

_, err = cli.RunCommand(ctx, "down", "--force", "--purge")
require.NoError(t, err)
}

func Test_CLI_InfraCreateAndDeleteUpperCase(t *testing.T) {
// running this test in parallel is ok as it uses a t.TempDir()
t.Parallel()
Expand Down
Loading

0 comments on commit 1cbed85

Please sign in to comment.