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 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
135 changes: 135 additions & 0 deletions cli/azd/pkg/azapi/stack_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"fmt"
"log"
"maps"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"time"

"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/armdeploymentstacks"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
Expand Down Expand Up @@ -751,3 +753,136 @@ 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,
},
}

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

poller, err := client.BeginValidateStackAtResourceGroup(ctxWithResp, resourceGroup, deploymentName, stack, nil)
if err != nil {
return validatePreflightError(rawResponse, err, "resource group")
}

_, err = poller.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
}

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,
},
}

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

poller, err := client.BeginValidateStackAtSubscription(ctxWithResp, deploymentName, stack, nil)
if err != nil {
return validatePreflightError(rawResponse, err, "subscription")
}

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

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)
hemarina marked this conversation as resolved.
Show resolved Hide resolved

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"`
}
hemarina marked this conversation as resolved.
Show resolved Hide resolved

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)
hemarina marked this conversation as resolved.
Show resolved Hide resolved
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