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 8 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
18 changes: 18 additions & 0 deletions cli/azd/pkg/azapi/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,24 @@ 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,
) error
ValidatePreflightToResourceGroup(
ctx context.Context,
subscriptionId,
resourceGroup,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
) error
WhatIfDeployToSubscription(
ctx context.Context,
subscriptionId string,
Expand Down
121 changes: 121 additions & 0 deletions cli/azd/pkg/azapi/stack_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,124 @@ 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,
) 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,
}
}

deleteBehavior := armdeploymentstacks.DeploymentStacksDeleteDetachEnumDelete

stack := armdeploymentstacks.DeploymentStack{
Tags: clonedTags,
Properties: &armdeploymentstacks.DeploymentStackProperties{
BypassStackOutOfSyncError: to.Ptr(false),
ActionOnUnmanage: &armdeploymentstacks.ActionOnUnmanage{
Resources: &deleteBehavior,
ManagementGroups: &deleteBehavior,
ResourceGroups: &deleteBehavior,
},
DenySettings: &armdeploymentstacks.DenySettings{
Mode: to.Ptr(armdeploymentstacks.DenySettingsModeNone),
},
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,
) 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,
}
}

deleteBehavior := armdeploymentstacks.DeploymentStacksDeleteDetachEnumDelete

stack := armdeploymentstacks.DeploymentStack{
Location: &location,
Tags: clonedTags,
Properties: &armdeploymentstacks.DeploymentStackProperties{
BypassStackOutOfSyncError: to.Ptr(false),
ActionOnUnmanage: &armdeploymentstacks.ActionOnUnmanage{
Resources: &deleteBehavior,
ManagementGroups: &deleteBehavior,
ResourceGroups: &deleteBehavior,
},
DenySettings: &armdeploymentstacks.DenySettings{
Mode: to.Ptr(armdeploymentstacks.DenySettingsModeNone),
},
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
}
127 changes: 127 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 @@ -683,3 +686,127 @@ 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,
) 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.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"`
}

func validatePreflightError(
rawResponse *http.Response,
err error,
typeMessage string,
) error {
if 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,
) 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)
return fmt.Errorf(
"validating preflight to resource group:\n\nDeployment Error Details:\n%w",
deploymentError,
)
}

return nil
}
35 changes: 28 additions & 7 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,24 @@ 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)
}

err = p.validatePreflight(
ctx,
bicepDeploymentData.Target,
bicepDeploymentData.CompiledBicep.RawArmTemplate,
bicepDeploymentData.CompiledBicep.Parameters,
deploymentTags,
)
Comment on lines +571 to +577
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 is great that the validate API exists but I am concerned that this may cause considerable time delay during deployments.

Do we have an idea of the amount of extra time the validations will take especially against more complex & nested deployments?

if err != nil {
return nil, err
}

cancelProgress := make(chan bool)
defer func() { cancelProgress <- true }()
go func() {
Expand Down Expand Up @@ -597,13 +615,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)
}
deployResult, err := p.deployModule(
ctx,
bicepDeploymentData.Target,
Expand Down Expand Up @@ -1709,6 +1720,16 @@ 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,
) error {
return target.ValidatePreflight(ctx, armTemplate, armParameters, tags)
}

// 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
21 changes: 21 additions & 0 deletions cli/azd/pkg/infra/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
OutputsUrl(ctx context.Context) (string, error)
// DeploymentUrl is the URL that may be used to view this deployment progress in the Azure Portal.
DeploymentUrl(ctx context.Context) (string, error)
// Validate a given template on preflight API
ValidatePreflight(
ctx context.Context,
template azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
) error
// Deploy a given template with a set of parameters.
Deploy(
ctx context.Context,
Expand Down Expand Up @@ -61,6 +68,13 @@
return s.name
}

func (s *ResourceGroupDeployment) ValidatePreflight(
ctx context.Context, template azure.RawArmTemplate, parameters azure.ArmParameters, tags map[string]*string,
) error {
return s.deploymentService.ValidatePreflightToResourceGroup(
ctx, s.subscriptionId, s.resourceGroupName, s.name, template, parameters, tags)
}

func (s *ResourceGroupDeployment) Deploy(
ctx context.Context, template azure.RawArmTemplate, parameters azure.ArmParameters, tags map[string]*string,
) (*azapi.ResourceDeployment, error) {
Expand Down Expand Up @@ -240,6 +254,13 @@
return s.deployment.DeploymentUrl, nil
}

func (s *SubscriptionDeployment) ValidatePreflight(
ctx context.Context, template azure.RawArmTemplate, parameters azure.ArmParameters, tags map[string]*string,
) error {
return s.deploymentService.ValidatePreflightToSubscription(ctx, s.subscriptionId, s.location,

Check failure on line 260 in cli/azd/pkg/infra/scope.go

View workflow job for this annotation

GitHub Actions / azd-lint (ubuntu-latest)

File is not `gofmt`-ed with `-s` (gofmt)

Check failure on line 260 in cli/azd/pkg/infra/scope.go

View workflow job for this annotation

GitHub Actions / azd-lint (windows-latest)

File is not `gofmt`-ed with `-s` (gofmt)
s.name, template, parameters, tags)
}

// Deploy a given template with a set of parameters.
func (s *SubscriptionDeployment) Deploy(
ctx context.Context,
Expand Down
Loading