Skip to content
131 changes: 95 additions & 36 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2636,10 +2636,19 @@ func (p *BicepProvider) validatePreflight(
if len(results) > 0 {
report := &ux.PreflightReport{}
for _, result := range results {
var links []ux.PreflightReportLink
for _, l := range result.Links {
links = append(links, ux.PreflightReportLink{
URL: l.URL,
Title: l.Title,
})
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
}
report.Items = append(report.Items, ux.PreflightReportItem{
IsError: result.Severity == PreflightCheckError,
DiagnosticID: result.DiagnosticID,
Message: result.Message,
Suggestion: result.Suggestion,
Links: links,
})
}
p.console.MessageUxItem(ctx, report)
Expand All @@ -2656,8 +2665,7 @@ func (p *BicepProvider) validatePreflight(
if report.HasWarnings() {
p.console.Message(ctx, "")
continueDeployment, promptErr := p.console.Confirm(ctx, input.ConsoleOptions{
Message: "Preflight validation found warnings that may cause the " +
"deployment to fail. Do you want to continue?",
Message: "Proceed with deployment despite the warnings above?",
DefaultValue: true,
})
if promptErr != nil {
Expand Down Expand Up @@ -2755,16 +2763,25 @@ func (p *BicepProvider) checkRoleAssignmentPermissions(
Severity: PreflightCheckWarning,
DiagnosticID: "role_assignment_missing",
Message: fmt.Sprintf(
"the current principal %s does not have permission to create role assignments "+
"%s on subscription %s. "+
"The deployment includes role assignments and will fail without this permission. "+
"Ensure you have the 'Role Based Access Control Administrator', "+
"'User Access Administrator', 'Owner', or a custom role with "+
"'Microsoft.Authorization/roleAssignments/write' assigned to your account.",
output.WithHighLightFormat("(%s)", principalId),
output.WithGrayFormat("(Microsoft.Authorization/roleAssignments/write)"),
"Principal %s lacks role assignment"+
" permissions on subscription %s\n"+
"The deployment includes role assignments"+
" and will fail without %s permission.",
output.WithHighLightFormat(
"(%s)", principalId),
output.WithHighLightFormat(subscriptionId),
output.WithGrayFormat(
"Microsoft.Authorization/"+
"roleAssignments/write"),
),
Suggestion: "Ensure you have the" +
" 'Role Based Access Control" +
" Administrator'," +
" 'User Access Administrator'," +
" 'Owner', or a custom role with" +
" 'Microsoft.Authorization/" +
"roleAssignments/write' assigned" +
" to your account.",
}}, nil
}

Expand All @@ -2773,14 +2790,17 @@ func (p *BicepProvider) checkRoleAssignmentPermissions(
Severity: PreflightCheckWarning,
DiagnosticID: "role_assignment_conditional",
Message: fmt.Sprintf(
"the current principal %s has conditional permission to create role "+
"assignments %s on "+
"subscription %s. The role assignment that grants this permission "+
"has an ABAC condition that may restrict which roles can be assigned. "+
"The deployment may fail if the condition does not permit the "+
"specific role assignments in the template.",
output.WithHighLightFormat("(%s)", principalId),
output.WithGrayFormat("(Microsoft.Authorization/roleAssignments/write)"),
"Principal %s has conditional role"+
" assignment permissions on"+
" subscription %s\n"+
"An ABAC condition may restrict"+
" which roles can be assigned."+
" The deployment may fail if the"+
" condition does not permit the"+
" specific role assignments in"+
" the template.",
output.WithHighLightFormat(
"(%s)", principalId),
output.WithHighLightFormat(subscriptionId),
),
}}, nil
Expand All @@ -2805,17 +2825,29 @@ func (p *BicepProvider) checkReservedResourceNames(
continue
}
for _, v := range findReservedResourceNameViolations(resource.Name) {
resourceName := output.WithHighLightFormat("%q", resource.Name)
resourceType := output.WithGrayFormat("(%s)", resource.Type)
link := output.WithLinkFormat(docsLink)
resourceName := output.WithHighLightFormat(
"%q", resource.Name)
resourceType := output.WithGrayFormat(
"(%s)", resource.Type)

results = append(results, PreflightCheckResult{
Severity: PreflightCheckWarning,
DiagnosticID: "reserved_resource_name",
Message: fmt.Sprintf(
"resource %s %s %s the reserved word %q. See %s.",
resourceName, resourceType, v.matchType, v.reservedWord, link,
"Resource %s %s %s the"+
" reserved word %q\n"+
"Azure does not allow reserved"+
" words in resource names."+
" The deployment will fail.",
resourceName, resourceType,
v.matchType, v.reservedWord,
),
Links: []PreflightCheckLink{
{
URL: docsLink,
Title: "Reserved resource name errors",
},
},
})
}
}
Expand Down Expand Up @@ -2924,16 +2956,25 @@ func (p *BicepProvider) checkAiModelQuota(
Severity: PreflightCheckWarning,
DiagnosticID: "ai_model_not_found",
Message: fmt.Sprintf(
"model %s%s was not found in the AI model "+
"catalog for %s. The deployment may fail "+
"if this model is not available. Verify "+
"the model name, SKU, and version are "+
"correct. See %s for supported models and regions.",
output.WithHighLightFormat(fmt.Sprintf("%q", dep.ModelName)),
"Model %s%s not found in %s\n"+
"Model not found in AI model catalog."+
" Provisioning will likely fail.",
output.WithHighLightFormat(
"%q", dep.ModelName),
output.WithGrayFormat(details),
output.WithHighLightFormat(loc),
output.WithLinkFormat("https://learn.microsoft.com/azure/ai-services/openai/concepts/models"),
),
Suggestion: "Verify the model name, SKU," +
" and version are correct.",
Links: []PreflightCheckLink{
{
URL: "https://learn.microsoft.com/" +
"azure/ai-services/openai/" +
"concepts/models",
Title: "Azure OpenAI supported" +
" models and regions",
},
},
})
continue
}
Expand Down Expand Up @@ -2968,20 +3009,38 @@ func (p *BicepProvider) checkAiModelQuota(
totalRequired := requiredByUsage[r.usageName]
if remaining < totalRequired {
reportedUsage[r.usageName] = true

suggestion := fmt.Sprintf(
"Reduce the requested capacity to %.0f"+
" or change your deployment location via %s."+
" You can also request a quota increase in the Azure portal.",
remaining,
output.WithHighLightFormat(
"azd env set AZURE_LOCATION <location>"),
)
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated

results = append(results, PreflightCheckResult{
Severity: PreflightCheckWarning,
DiagnosticID: "ai_model_quota_exceeded",
Message: fmt.Sprintf(
"insufficient quota for model %s %s in %s. "+
"Requested capacity: %.0f, remaining quota: %.0f. "+
"The deployment may fail. Consider reducing capacity, "+
"selecting a different model, or requesting a quota increase.",
output.WithHighLightFormat("%q", r.dep.ModelName),
output.WithGrayFormat("(SKU: %s)", r.dep.SkuName),
"Insufficient quota for model %s %s"+
" in %s\n"+
"Requested: %.0f · Available: %.0f",
output.WithHighLightFormat(
"%q", r.dep.ModelName),
output.WithGrayFormat(
"(SKU: %s)", r.dep.SkuName),
output.WithHighLightFormat(loc),
totalRequired,
remaining,
),
Suggestion: suggestion,
Links: []PreflightCheckLink{
{
URL: "https://learn.microsoft.com/azure/quotas/quickstart-increase-quota-portal",
Title: "Increase Azure subscription quotas",
},
},
})
}
}
Expand Down
13 changes: 13 additions & 0 deletions cli/azd/pkg/infra/provisioning/bicep/local_preflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ type PreflightCheckResult struct {
DiagnosticID string
// Message is a human-readable description of the finding.
Message string
// Suggestion is an optional actionable recommendation for resolving the issue.
// It should be dynamically generated with context-specific advice when possible.
Suggestion string
// Links is an optional list of reference links related to the finding.
Links []PreflightCheckLink
}

// PreflightCheckLink represents a reference link attached to a preflight check result.
type PreflightCheckLink struct {
// URL is the link target.
URL string
// Title is the display text (optional — if empty, the URL is shown).
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
Title string
}

// validationContext provides the data and utilities available to preflight check functions.
Expand Down
119 changes: 114 additions & 5 deletions cli/azd/pkg/output/ux/preflight_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
package ux

import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"

"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/mattn/go-colorable"
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
)

// PreflightReportItem represents a single finding from preflight validation.
Expand All @@ -20,6 +23,18 @@ type PreflightReportItem struct {
DiagnosticID string
// Message describes the finding.
Message string
// Suggestion is an optional actionable recommendation for resolving the issue.
Suggestion string
// Links is an optional list of reference links related to the finding.
Links []PreflightReportLink
}

// PreflightReportLink represents a reference link attached to a preflight report item.
type PreflightReportLink struct {
// URL is the link target.
URL string
// Title is the display text (optional — if empty, the URL is shown).
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
Title string
}

// PreflightReport displays the results of local preflight validation.
Expand All @@ -40,7 +55,7 @@ func (r *PreflightReport) ToString(currentIndentation string) string {
if i > 0 {
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("%s%s %s", currentIndentation, warningPrefix, w.Message))
writeItem(&sb, currentIndentation, warningPrefix, w)
Comment thread
vhvb1989 marked this conversation as resolved.
}

if len(warnings) > 0 && len(errors) > 0 {
Expand All @@ -51,18 +66,95 @@ func (r *PreflightReport) ToString(currentIndentation string) string {
if i > 0 {
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("%s%s %s", currentIndentation, failedPrefix, e.Message))
writeItem(&sb, currentIndentation, failedPrefix, e)
}

return sb.String()
}

// writeItem renders a single report item with multi-line support.
// The first line is prefixed with the status indicator (e.g. "(!) Warning:").
// Continuation lines in the message are indented at the same level as the prefix.
func writeItem(
sb *strings.Builder, indent string, prefix string, item PreflightReportItem,
) {
lines := strings.Split(item.Message, "\n")
sb.WriteString(fmt.Sprintf("%s%s %s", indent, prefix, lines[0]))
for _, line := range lines[1:] {
Comment thread
vhvb1989 marked this conversation as resolved.
sb.WriteString(fmt.Sprintf("\n%s%s", indent, line))
Comment thread
vhvb1989 marked this conversation as resolved.
}

if item.Suggestion != "" {
sb.WriteString(fmt.Sprintf("\n%s%s %s",
indent,
output.WithHighLightFormat("Suggestion:"),
item.Suggestion))
}
for _, link := range item.Links {
if link.Title != "" {
sb.WriteString(fmt.Sprintf("\n%s• %s",
indent,
output.WithHyperlink(link.URL, link.Title)))
} else {
sb.WriteString(fmt.Sprintf("\n%s• %s",
indent,
output.WithLinkFormat(link.URL)))
}
}
}

func (r *PreflightReport) MarshalJSON() ([]byte, error) {
warnings, errors := r.partition()

return json.Marshal(output.EventForMessage(
fmt.Sprintf("preflight: %d warning(s), %d error(s)",
len(warnings), len(errors))))
type jsonLink struct {
URL string `json:"url"`
Title string `json:"title,omitempty"`
}
type jsonItem struct {
Severity string `json:"severity"`
DiagnosticID string `json:"diagnosticId,omitempty"`
Message string `json:"message"`
Suggestion string `json:"suggestion,omitempty"`
Links []jsonLink `json:"links,omitempty"`
}

// Use partition ordering (warnings first, then errors)
// to match ToString() output order.
ordered := make(
[]PreflightReportItem, 0, len(warnings)+len(errors))
ordered = append(ordered, warnings...)
ordered = append(ordered, errors...)

items := make([]jsonItem, 0, len(ordered))
for _, item := range ordered {
severity := "warning"
if item.IsError {
severity = "error"
}
ji := jsonItem{
Severity: severity,
DiagnosticID: item.DiagnosticID,
Message: stripAnsi(item.Message),
Suggestion: stripAnsi(item.Suggestion),
}
for _, link := range item.Links {
ji.Links = append(ji.Links, jsonLink(link))
}
items = append(items, ji)
}

result := struct {
Type string `json:"type"`
Summary string `json:"summary"`
Items []jsonItem `json:"items"`
}{
Type: "preflight",
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
Summary: fmt.Sprintf("preflight: %d warning(s), %d error(s)",
len(warnings), len(errors)),
Items: items,
}

return json.Marshal(result)
}

// HasErrors returns true if the report contains at least one error-level item.
Expand Down Expand Up @@ -96,3 +188,20 @@ func (r *PreflightReport) partition() (warnings, errors []PreflightReportItem) {
}
return warnings, errors
}

// stripAnsi removes ANSI escape sequences from a string for
// machine-readable output (e.g. JSON).
func stripAnsi(s string) string {
if s == "" {
return s
}
var buf bytes.Buffer
// colorable.NewNonColorable strips ANSI sequences.
if _, err := io.Copy(
colorable.NewNonColorable(&buf),
strings.NewReader(s),
); err != nil {
return s
}
return buf.String()
}
Loading
Loading