Skip to content

Commit

Permalink
Log progress of resources created during azd provision (#152)
Browse files Browse the repository at this point in the history
Azure resources created are logged to standard output (in interactive mode).
  • Loading branch information
weikanglim authored Jul 28, 2022
1 parent bad5ed4 commit c8cdad1
Show file tree
Hide file tree
Showing 10 changed files with 427 additions and 40 deletions.
1 change: 1 addition & 0 deletions cli/azd/.vscode/cspell-azd-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pyapp
keychain
restoreapp
rzip
serverfarms
sstore
staticwebapp
structs
Expand Down
1 change: 1 addition & 0 deletions cli/azd/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features Added

- [[#100]](https://github.com/Azure/azure-dev/pull/100) Add support for an optional `docker` section in service configuration to control advanced docker options.
- [[#152]](https://github.com/Azure/azure-dev/pull/152) While provisioning in interactive mode (default), Azure resources are now logged to console as they are created.

### Breaking Changes

Expand Down
34 changes: 8 additions & 26 deletions cli/azd/cmd/infra_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/iac/bicep"
"github.com/azure/azure-dev/cli/azd/pkg/infra"
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/project"
Expand Down Expand Up @@ -199,14 +200,16 @@ func (ica *infraCreateAction) Run(ctx context.Context, cmd *cobra.Command, args
}
var res deployFuncResult

deployAndReportProgress := func(showProgress func(string)) error {
deployAndReportProgress := func(spinner *spin.Spinner) error {
deployResChan := make(chan deployFuncResult)
go func() {
res, err := bicep.Deploy(ctx, deploymentTarget, bicepPath, azdCtx.BicepParametersFilePath(ica.rootOptions.EnvironmentName, "main"))
deployResChan <- deployFuncResult{Result: res, Err: err}
close(deployResChan)
}()

progressDisplay := provisioning.NewProvisioningProgressDisplay(infra.NewAzureResourceManager(azCli), env.GetSubscriptionId(), env.GetEnvName())

for {
select {
case deployRes := <-deployResChan:
Expand All @@ -217,7 +220,7 @@ func (ica *infraCreateAction) Run(ctx context.Context, cmd *cobra.Command, args
continue
}
if interactive {
reportDeploymentStatusInteractive(ctx, azCli, env, showProgress)
progressDisplay.ReportProgress(ctx, spinner.Title, spinner.Println)
} else {
reportDeploymentStatusJson(ctx, azCli, env, formatter, cmd)
}
Expand All @@ -236,7 +239,7 @@ func (ica *infraCreateAction) Run(ctx context.Context, cmd *cobra.Command, args

spinner := spin.NewSpinner("Creating Azure resources")
spinner.Start()
err = deployAndReportProgress(spinner.Title)
err = deployAndReportProgress(spinner)
spinner.Stop()

if err == nil {
Expand Down Expand Up @@ -275,27 +278,6 @@ func (ica *infraCreateAction) Run(ctx context.Context, cmd *cobra.Command, args
return nil
}

func reportDeploymentStatusInteractive(ctx context.Context, azCli tools.AzCli, env environment.Environment, showProgress func(string)) {
resourceManager := infra.NewAzureResourceManager(azCli)

operations, err := resourceManager.GetDeploymentResourceOperations(ctx, env.GetSubscriptionId(), env.GetEnvName())
if err != nil {
// Status display is best-effort activity.
return
}

succeededCount := 0

for _, resourceOperation := range *operations {
if resourceOperation.Properties.ProvisioningState == "Succeeded" {
succeededCount++
}
}

status := fmt.Sprintf("Creating Azure resources (%d of ~%d completed)", succeededCount, len(*operations))
showProgress(status)
}

type progressReport struct {
Timestamp time.Time `json:"timestamp"`
Operations []tools.AzCliResourceOperation `json:"operations"`
Expand All @@ -305,14 +287,14 @@ func reportDeploymentStatusJson(ctx context.Context, azCli tools.AzCli, env envi
resourceManager := infra.NewAzureResourceManager(azCli)

ops, err := resourceManager.GetDeploymentResourceOperations(ctx, env.GetSubscriptionId(), env.GetEnvName())
if err != nil || len(*ops) == 0 {
if err != nil || len(ops) == 0 {
// Status display is best-effort activity.
return
}

report := progressReport{
Timestamp: time.Now(),
Operations: *ops,
Operations: ops,
}

_ = formatter.Format(report, cmd.OutOrStdout(), nil)
Expand Down
39 changes: 36 additions & 3 deletions cli/azd/pkg/infra/azure_resource_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func NewAzureResourceManager(azCli tools.AzCli) *AzureResourceManager {
}
}

func (rm *AzureResourceManager) GetDeploymentResourceOperations(ctx context.Context, subscriptionId string, deploymentName string) (*[]tools.AzCliResourceOperation, error) {
func (rm *AzureResourceManager) GetDeploymentResourceOperations(ctx context.Context, subscriptionId string, deploymentName string) ([]tools.AzCliResourceOperation, error) {
// Gets all the subscription level deployments
subOperations, err := rm.azCli.ListSubscriptionDeploymentOperations(ctx, subscriptionId, deploymentName)
if err != nil {
Expand All @@ -38,7 +38,7 @@ func (rm *AzureResourceManager) GetDeploymentResourceOperations(ctx context.Cont
resourceOperations := []tools.AzCliResourceOperation{}

if strings.TrimSpace(resourceGroupName) == "" {
return &resourceOperations, nil
return resourceOperations, nil
}

// Find all resource group deployments within the subscription operations
Expand All @@ -52,7 +52,7 @@ func (rm *AzureResourceManager) GetDeploymentResourceOperations(ctx context.Cont
}
}

return &resourceOperations, nil
return resourceOperations, nil
}

func (rm *AzureResourceManager) appendDeploymentResourcesRecursive(ctx context.Context, subscriptionId string, resourceGroupName string, deploymentName string, resourceOperations *[]tools.AzCliResourceOperation) error {
Expand All @@ -74,3 +74,36 @@ func (rm *AzureResourceManager) appendDeploymentResourcesRecursive(ctx context.C

return nil
}

func (rm *AzureResourceManager) GetResourceTypeDisplayName(ctx context.Context, subscriptionId string, resourceId string, resourceType AzureResourceType) (string, error) {
if resourceType == AzureResourceTypeWebSite {
// Web apps have different kinds of resources sharing the same resource type 'Microsoft.Web/sites', i.e. Function app vs. App service
// It is extremely important that we display the right one, thus we resolve it by querying the properties of the ARM resource.
resourceTypeDisplayName, err := rm.GetWebAppResourceTypeDisplayName(ctx, subscriptionId, resourceId)

if err != nil {
return "", err
} else {
return resourceTypeDisplayName, nil
}
} else {
resourceTypeDisplayName := GetResourceTypeDisplayName(resourceType)
return resourceTypeDisplayName, nil
}
}

func (rm *AzureResourceManager) GetWebAppResourceTypeDisplayName(ctx context.Context, subscriptionId string, resourceId string) (string, error) {
resource, err := rm.azCli.GetResource(ctx, subscriptionId, resourceId)

if err != nil {
return "", fmt.Errorf("getting web app resource type display names: %w", err)
}

if strings.Contains(resource.Kind, "functionapp") {
return "Function App", nil
} else if strings.Contains(resource.Kind, "app") {
return "App Service", nil
} else {
return "Web App", nil
}
}
6 changes: 3 additions & 3 deletions cli/azd/pkg/infra/azure_resource_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func TestGetDeploymentResourceOperationsSuccess(t *testing.T) {
require.NotNil(t, operations)
require.Nil(t, err)

require.Len(t, *operations, 2)
require.Len(t, operations, 2)
require.Equal(t, 1, subCalls)
require.Equal(t, 1, groupCalls)
}
Expand Down Expand Up @@ -220,7 +220,7 @@ func TestGetDeploymentResourceOperationsNoResourceGroup(t *testing.T) {

require.NotNil(t, operations)
require.Nil(t, err)
require.Len(t, *operations, 0)
require.Len(t, operations, 0)
require.Equal(t, 1, subCalls)
require.Equal(t, 0, groupCalls)
}
Expand Down Expand Up @@ -260,7 +260,7 @@ func TestGetDeploymentResourceOperationsWithNestedDeployments(t *testing.T) {

require.NotNil(t, operations)
require.Nil(t, err)
require.Len(t, *operations, 4)
require.Len(t, operations, 4)
require.Equal(t, 1, subCalls)
require.Equal(t, 2, groupCalls)
}
Expand Down
78 changes: 70 additions & 8 deletions cli/azd/pkg/infra/azure_resource_types.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,76 @@
package infra

import "strings"

type AzureResourceType string

const (
AzureResourceTypeResourceGroup AzureResourceType = "Microsoft.Resources/resourceGroups"
AzureResourceTypeDeployment AzureResourceType = "Microsoft.Resources/deployments"
AzureResourceTypeStorageAccount AzureResourceType = "Microsoft.Storage/storageAccounts"
AzureResourceTypeKeyVault AzureResourceType = "Microsoft.KeyVault/vaults"
AzureResourceTypePortalDashboard AzureResourceType = "Microsoft.Portal/dashboards"
AzureResourceTypeAppInsightComponent AzureResourceType = "Microsoft.Insights/components"
AzureResourceTypeWebSite AzureResourceType = "Microsoft.Web/sites"
AzureResourceTypeContainerApp AzureResourceType = "Microsoft.App/containerApps"
AzureResourceTypeResourceGroup AzureResourceType = "Microsoft.Resources/resourceGroups"
AzureResourceTypeDeployment AzureResourceType = "Microsoft.Resources/deployments"
AzureResourceTypeStorageAccount AzureResourceType = "Microsoft.Storage/storageAccounts"
AzureResourceTypeKeyVault AzureResourceType = "Microsoft.KeyVault/vaults"
AzureResourceTypePortalDashboard AzureResourceType = "Microsoft.Portal/dashboards"
AzureResourceTypeAppInsightComponent AzureResourceType = "Microsoft.Insights/components"
AzureResourceTypeLogAnalyticsWorkspace AzureResourceType = "Microsoft.OperationalInsights/workspaces"
AzureResourceTypeWebSite AzureResourceType = "Microsoft.Web/sites"
AzureResourceTypeStaticWebSite AzureResourceType = "Microsoft.Web/staticSites"
AzureResourceTypeServicePlan AzureResourceType = "Microsoft.Web/serverfarms"
AzureResourceTypeSqlDatabase AzureResourceType = "Microsoft.Sql/servers"
AzureResourceTypeCosmosDb AzureResourceType = "Microsoft.DocumentDB/databaseAccounts"
AzureResourceTypeContainerApp AzureResourceType = "Microsoft.App/containerApps"
AzureResourceTypeContainerAppEnvironment AzureResourceType = "Microsoft.App/managedEnvironments"
)

const resourceLevelSeparator = "/"

// GetResourceTypeDisplayName retrieves the display name for the given resource type.
// If the display name was not found for the given resource type, an empty string is returned instead.
func GetResourceTypeDisplayName(resourceType AzureResourceType) string {
// Azure Resource Manager does not offer an API for obtaining display name for resource types.
// Display names for Azure resource types in Azure Portal are encoded in UX definition files instead.
// As a result, we provide static translations for known resources below. These are obtained from the Azure Portal.
switch resourceType {
case AzureResourceTypeResourceGroup:
return "Resource group"
case AzureResourceTypeStorageAccount:
return "Storage account"
case AzureResourceTypeKeyVault:
return "Key vault"
case AzureResourceTypePortalDashboard:
return "Portal dashboard"
case AzureResourceTypeAppInsightComponent:
return "Application Insights"
case AzureResourceTypeLogAnalyticsWorkspace:
return "Log Analytics workspace"
case AzureResourceTypeWebSite:
return "Web App"
case AzureResourceTypeStaticWebSite:
return "Static Web App"
case AzureResourceTypeContainerApp:
return "Container App"
case AzureResourceTypeContainerAppEnvironment:
return "Container Apps Environment"
case AzureResourceTypeServicePlan:
return "App Service plan"
case AzureResourceTypeCosmosDb:
return "Azure Cosmos DB"
}

return ""
}

// IsTopLevelResourceType returns true if the resource type is a top-level resource type, otherwise false.
// A top-level resource type is of the format of: {ResourceProvider}/{TopLevelResourceType}, i.e. Microsoft.DocumentDB/databaseAccounts
func IsTopLevelResourceType(resourceType AzureResourceType) bool {
resType := string(resourceType)
firstIndex := strings.Index(resType, resourceLevelSeparator)

if firstIndex == -1 ||
firstIndex == 0 ||
firstIndex == len(resType)-1 {
return false
}

// Should not contain second separator
return !strings.Contains(resType[firstIndex+1:], resourceLevelSeparator)
}
33 changes: 33 additions & 0 deletions cli/azd/pkg/infra/azure_resource_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package infra

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsTopLevelResourceType(t *testing.T) {
var tests = []struct {
resourceType string
result bool
}{
{"", false},
{"/", false},
{"/foo", false},
{"foo", false},
{"foo/", false},
{"foo/b", true},
{"foo/bar", true},
{"foo/bar/baz", false},
{"foo/bar/", false},
{"Microsoft.Storage/storageAccounts", true},
{"Microsoft.DocumentDB/databaseAccounts/collections", false},
}

for _, test := range tests {
t.Run(fmt.Sprintf("\"%s\")", test.resourceType), func(t *testing.T) {
assert.Equal(t, test.result, IsTopLevelResourceType(AzureResourceType(test.resourceType)))
})
}
}
Loading

0 comments on commit c8cdad1

Please sign in to comment.