Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Preflight Validate API support #4329

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
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
20 changes: 20 additions & 0 deletions cli/azd/pkg/azapi/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ type DeploymentService interface {
resourceGroupName string,
deploymentName string,
) ([]*armresources.DeploymentOperation, error)
ValidatePreflightToSubscription(
ctx context.Context,
subscriptionId string,
location string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error
ValidatePreflightToResourceGroup(
ctx context.Context,
subscriptionId,
resourceGroup,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error
WhatIfDeployToSubscription(
ctx context.Context,
subscriptionId string,
Expand Down
117 changes: 117 additions & 0 deletions cli/azd/pkg/azapi/stack_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,3 +743,120 @@ func convertFromStacksProvisioningState(

return DeploymentProvisioningState("")
}

func (d *StackDeployments) ValidatePreflightToResourceGroup(
ctx context.Context,
subscriptionId string,
resourceGroup string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return err
}

templateHash, err := d.CalculateTemplateHash(ctx, subscriptionId, armTemplate)
if err != nil {
return fmt.Errorf("failed to calculate template hash: %w", err)
}

clonedTags := maps.Clone(tags)
clonedTags[azure.TagKeyAzdDeploymentTemplateHashName] = &templateHash

stackParams := map[string]*armdeploymentstacks.DeploymentParameter{}
for k, v := range parameters {
stackParams[k] = &armdeploymentstacks.DeploymentParameter{
Value: v.Value,
}
}

deploymentStackOptions, err := parseDeploymentStackOptions(options)
if err != nil {
return err
}

stack := armdeploymentstacks.DeploymentStack{
Tags: clonedTags,
Properties: &armdeploymentstacks.DeploymentStackProperties{
BypassStackOutOfSyncError: deploymentStackOptions.BypassStackOutOfSyncError,
ActionOnUnmanage: deploymentStackOptions.ActionOnUnmanage,
DenySettings: deploymentStackOptions.DenySettings,
Parameters: stackParams,
Template: armTemplate,
},
}
poller, err := client.BeginValidateStackAtResourceGroup(ctx, resourceGroup, deploymentName, stack, nil)
if err != nil {
return err
}

_, err = poller.PollUntilDone(ctx, nil)
if err != nil {
return err
}

return nil
}

func (d *StackDeployments) ValidatePreflightToSubscription(
ctx context.Context,
subscriptionId string,
location string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return err
}

templateHash, err := d.CalculateTemplateHash(ctx, subscriptionId, armTemplate)
if err != nil {
return fmt.Errorf("failed to calculate template hash: %w", err)
}

clonedTags := maps.Clone(tags)
clonedTags[azure.TagKeyAzdDeploymentTemplateHashName] = &templateHash

stackParams := map[string]*armdeploymentstacks.DeploymentParameter{}
for k, v := range parameters {
stackParams[k] = &armdeploymentstacks.DeploymentParameter{
Value: v.Value,
}
}

deploymentStackOptions, err := parseDeploymentStackOptions(options)
if err != nil {
return err
}

stack := armdeploymentstacks.DeploymentStack{
Location: &location,
Tags: clonedTags,
Properties: &armdeploymentstacks.DeploymentStackProperties{
BypassStackOutOfSyncError: deploymentStackOptions.BypassStackOutOfSyncError,
ActionOnUnmanage: deploymentStackOptions.ActionOnUnmanage,
DenySettings: deploymentStackOptions.DenySettings,
Parameters: stackParams,
Template: armTemplate,
},
}
poller, err := client.BeginValidateStackAtSubscription(ctx, deploymentName, stack, nil)
if err != nil {
return err
}

_, err = poller.PollUntilDone(ctx, nil)
if err != nil {
return err
}

return nil
}
129 changes: 129 additions & 0 deletions cli/azd/pkg/azapi/standard_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/azure/azure-dev/cli/azd/pkg/account"
Expand Down Expand Up @@ -688,3 +691,129 @@ func convertFromStandardProvisioningState(state armresources.ProvisioningState)

return DeploymentProvisioningState("")
}

func (ds *StandardDeployments) ValidatePreflightToSubscription(
ctx context.Context,
subscriptionId string,
location string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error {
deploymentClient, err := ds.createDeploymentsClient(ctx, subscriptionId)
if err != nil {
return fmt.Errorf("creating deployments client: %w", err)
}

var rawResponse *http.Response
ctxWithResp := runtime.WithCaptureResponse(ctx, &rawResponse)
Comment on lines +710 to +711
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to do this? If we get an HttpError we should be able to case the error to ARM http error to parse the error message.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's a workaround for a related bug here: Azure/azure-sdk-for-go#23350


validate, err := deploymentClient.BeginValidateAtSubscriptionScope(
ctxWithResp, deploymentName,
armresources.Deployment{
Properties: &armresources.DeploymentProperties{
Template: armTemplate,
Parameters: parameters,
Mode: to.Ptr(armresources.DeploymentModeIncremental),
},
Location: to.Ptr(location),
Tags: tags,
}, nil)
if err != nil {
return validatePreflightError(rawResponse, err, "subscription")
}

_, err = validate.PollUntilDone(ctx, nil)
if err != nil {
preflightError := createDeploymentError(err)
return fmt.Errorf(
"validating preflight to subscription:\n\nPreflight Error Details:\n%w",
preflightError,
)
}

return nil
}

type PreflightErrorResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details []struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"details"`
} `json:"error"`
}
Comment on lines +740 to +749
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this basically captures the same ARM errors that we use in the AzureDeploymentError object that we already have. Can we reuse that one instead?


func validatePreflightError(
rawResponse *http.Response,
err error,
typeMessage string,
) error {
if rawResponse == nil || rawResponse.StatusCode != 400 {
return fmt.Errorf("calling preflight validate api failing to %s: %w", typeMessage, err)
}

defer rawResponse.Body.Close()
body, errOnRawResponse := io.ReadAll(rawResponse.Body)
if errOnRawResponse != nil {
return fmt.Errorf("failed to read response error body from preflight api to %s: %w", typeMessage, errOnRawResponse)
}

var errPreflight PreflightErrorResponse
errOnRawResponse = json.Unmarshal(body, &errPreflight)
if errOnRawResponse != nil {
return fmt.Errorf("failed to unmarshal preflight error response to %s: %w", typeMessage, errOnRawResponse)
}

if len(errPreflight.Error.Details) > 0 {
detailMessage := errPreflight.Error.Details[0].Message
return fmt.Errorf("calling preflight validate api failing to %s: %s", typeMessage, detailMessage)
} else {
return fmt.Errorf("calling preflight validate api failing to %s: %w", typeMessage, err)
}
}

func (ds *StandardDeployments) ValidatePreflightToResourceGroup(
ctx context.Context,
subscriptionId, resourceGroup, deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error {
deploymentClient, err := ds.createDeploymentsClient(ctx, subscriptionId)
if err != nil {
return fmt.Errorf("creating deployments client: %w", err)
}

var rawResponse *http.Response
ctxWithResp := runtime.WithCaptureResponse(ctx, &rawResponse)

validate, err := deploymentClient.BeginValidate(ctxWithResp, resourceGroup, deploymentName,
armresources.Deployment{
Properties: &armresources.DeploymentProperties{
Template: armTemplate,
Parameters: parameters,
Mode: to.Ptr(armresources.DeploymentModeIncremental),
},
Tags: tags,
}, nil)
if err != nil {
return validatePreflightError(rawResponse, err, "resource group")
}

_, err = validate.PollUntilDone(ctx, nil)
if err != nil {
deploymentError := createDeploymentError(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use this same createDeploymentError() func in the deployment stack use cases as well.

return fmt.Errorf(
"validating preflight to resource group:\n\nDeployment Error Details:\n%w",
deploymentError,
)
}

return nil
}
47 changes: 35 additions & 12 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,30 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult,
logDS("%s", err.Error())
}

deploymentTags := map[string]*string{
azure.TagKeyAzdEnvName: to.Ptr(p.env.Name()),
}
if parametersHashErr == nil {
deploymentTags[azure.TagKeyAzdDeploymentStateParamHashName] = to.Ptr(currentParamsHash)
}

optionsMap, err := convert.ToMap(p.options)
if err != nil {
return nil, err
}

err = p.validatePreflight(
ctx,
bicepDeploymentData.Target,
bicepDeploymentData.CompiledBicep.RawArmTemplate,
bicepDeploymentData.CompiledBicep.Parameters,
deploymentTags,
optionsMap,
)
if err != nil {
return nil, err
}

cancelProgress := make(chan bool)
defer func() { cancelProgress <- true }()
go func() {
Expand Down Expand Up @@ -593,18 +617,6 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult,
// Start the deployment
p.console.ShowSpinner(ctx, "Creating/Updating resources", input.Step)

deploymentTags := map[string]*string{
azure.TagKeyAzdEnvName: to.Ptr(p.env.Name()),
}
if parametersHashErr == nil {
deploymentTags[azure.TagKeyAzdDeploymentStateParamHashName] = to.Ptr(currentParamsHash)
}

optionsMap, err := convert.ToMap(p.options)
if err != nil {
return nil, err
}

deployResult, err := p.deployModule(
ctx,
bicepDeploymentData.Target,
Expand Down Expand Up @@ -1718,6 +1730,17 @@ func (p *BicepProvider) convertToDeployment(bicepTemplate azure.ArmTemplate) (*p
return &template, nil
}

func (p *BicepProvider) validatePreflight(
ctx context.Context,
target infra.Deployment,
armTemplate azure.RawArmTemplate,
armParameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error {
return target.ValidatePreflight(ctx, armTemplate, armParameters, tags, options)
}

// Deploys the specified Bicep module and parameters with the selected provisioning scope (subscription vs resource group)
func (p *BicepProvider) deployModule(
ctx context.Context,
Expand Down
Loading
Loading