Skip to content
Draft
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
1 change: 1 addition & 0 deletions cli/azd/cmd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {

// Required for nested actions called from composite actions like 'up'
registerAction[*cmd.ProvisionAction](container, "azd-provision-action")
registerAction[*cmd.ValidateAction](container, "azd-validate-action")
registerAction[*downAction](container, "azd-down-action")
registerAction[*configShowAction](container, "azd-config-show-action")
}
Expand Down
14 changes: 14 additions & 0 deletions cli/azd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,20 @@ func newRootCmd(
}).
UseMiddleware("extensions", middleware.NewExtensionsMiddleware)

root.
Add("validate", &actions.ActionDescriptorOptions{
Command: cmd.NewValidateCmd(),
FlagsResolver: cmd.NewValidateFlags,
ActionResolver: cmd.NewValidateAction,
OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat},
DefaultFormat: output.NoneFormat,
GroupingOptions: actions.CommandGroupOptions{
RootLevelHelp: actions.CmdGroupAzure,
},
RequireLogin: true,
}).
UseMiddleware("extensions", middleware.NewExtensionsMiddleware)

root.
Add("package", &actions.ActionDescriptorOptions{
Command: newPackageCmd(),
Expand Down
19 changes: 19 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6726,6 +6726,21 @@ const completionSpec: Fig.Spec = {
},
],
},
{
name: ['validate'],
description: 'Validate your project configuration and Azure readiness.',
options: [
{
name: ['--gate'],
description: 'Run only the specified validation gate (e.g. "local-preflight").',
args: [
{
name: 'gate',
},
],
},
],
},
{
name: ['version'],
description: 'Print the version number of Azure Developer CLI.',
Expand Down Expand Up @@ -8010,6 +8025,10 @@ const completionSpec: Fig.Spec = {
name: ['update'],
description: 'Updates azd to the latest version.',
},
{
name: ['validate'],
description: 'Validate your project configuration and Azure readiness.',
},
{
name: ['version'],
description: 'Print the version number of Azure Developer CLI.',
Expand Down
20 changes: 20 additions & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-validate.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

Validate your project configuration and Azure readiness.

Usage
azd validate [flags]

Flags
-e, --environment string : The name of the environment to use.
--gate string : Run only the specified validation gate (e.g. "local-preflight").

Global Flags
-C, --cwd string : Sets the current working directory.
--debug : Enables debugging and diagnostics logging.
--docs : Opens the documentation for azd validate in your web browser.
-h, --help : Gets help for validate.
--no-prompt : Runs without prompts. Uses existing values; fails if any required value or decision cannot be resolved automatically.

Find a bug? Want to let us know how we're doing? Fill out this brief survey: https://aka.ms/azure-dev/hats.


1 change: 1 addition & 0 deletions cli/azd/cmd/testdata/TestUsage-azd.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Commands
down : Delete your project's Azure resources.
provision : Provision Azure resources for your project.
publish : Publish a service to a container registry.
validate : Validate your project configuration and Azure readiness.

Manage and show settings
completion : Generate shell completion scripts.
Expand Down
4 changes: 4 additions & 0 deletions cli/azd/internal/cmd/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func (p *mockProvider) Destroy(_ context.Context, _ provisioning.DestroyOptions)

func (p *mockProvider) EnsureEnv(_ context.Context) error { return nil }

func (p *mockProvider) Validate(_ context.Context) (*provisioning.ValidateResult, error) {
return &provisioning.ValidateResult{Skipped: true, SkipReason: "mock"}, nil
}

func (p *mockProvider) Parameters(_ context.Context) ([]provisioning.Parameter, error) {
return nil, nil
}
Expand Down
201 changes: 201 additions & 0 deletions cli/azd/internal/cmd/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"fmt"
"io"

"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/validate"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// ValidateFlags holds the flags for the validate command.
type ValidateFlags struct {
global *internal.GlobalCommandOptions
// gate filters the pipeline to run only the specified gate.
gate string
*internal.EnvFlag
}

// Bind registers the validate flags on the given flag set.
func (f *ValidateFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.EnvFlag = &internal.EnvFlag{}
f.EnvFlag.Bind(local, global)
local.StringVar(
&f.gate,
"gate",
"",
"Run only the specified validation gate (e.g. \"local-preflight\").",
)
f.global = global
}

// NewValidateFlags creates and binds flags for the validate command.
func NewValidateFlags(
cmd *cobra.Command, global *internal.GlobalCommandOptions,
) *ValidateFlags {
flags := &ValidateFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}

// NewValidateCmd creates the cobra command for "azd validate".
func NewValidateCmd() *cobra.Command {
return &cobra.Command{
Use: "validate",
Short: "Validate your project configuration and Azure readiness.",
Long: `Validate runs a pipeline of validation gates against your project
and environment to detect issues before provisioning. Each gate performs
a set of checks (e.g. role assignments, resource quotas, configuration)
and reports warnings or errors with actionable suggestions.

Use --gate to run a specific gate in isolation.`,
}
}

// ValidateAction implements the azd validate command.
type ValidateAction struct {
flags *ValidateFlags
projectConfig *project.ProjectConfig
projectManager project.ProjectManager
provisionManager *provisioning.Manager
env *environment.Environment
console input.Console
formatter output.Formatter
writer io.Writer
gates []validate.Gate
}

// NewValidateAction creates a new ValidateAction with all dependencies
// injected via IoC.
func NewValidateAction(
flags *ValidateFlags,
projectConfig *project.ProjectConfig,
projectManager project.ProjectManager,
provisionManager *provisioning.Manager,
env *environment.Environment,
console input.Console,
formatter output.Formatter,
writer io.Writer,
) actions.Action {
action := &ValidateAction{
flags: flags,
projectConfig: projectConfig,
projectManager: projectManager,
provisionManager: provisionManager,
env: env,
console: console,
formatter: formatter,
writer: writer,
}

// Register built-in gates
action.RegisterGate(validate.NewProjectConfigGate())
action.RegisterGate(
validate.NewLocalPreflightGate(provisionManager))

return action
}

// RegisterGate adds a validation gate to be executed by this action.
// Gates are executed in the order they are registered.
func (a *ValidateAction) RegisterGate(gate validate.Gate) {
a.gates = append(a.gates, gate)
}

// Run executes the validation pipeline and displays results.
func (a *ValidateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
a.console.MessageUxItem(ctx, &ux.MessageTitle{
Title: "Validating project (azd validate)",
TitleNote: "Running validation checks against your project and environment",
})

if err := a.projectManager.Initialize(ctx, a.projectConfig); err != nil {
return nil, err
}

pipeline := validate.NewPipeline(validate.PipelineOptions{
OnError: validate.OnErrorContinue,
})

// Register gates, optionally filtering by --gate flag
gateFilter := a.flags.gate
registered := 0
for _, gate := range a.gates {
if gateFilter != "" && gate.Name() != gateFilter {
continue
}
pipeline.AddGate(gate)
registered++
}

if gateFilter != "" && registered == 0 {
return nil, fmt.Errorf(
"unknown gate %q; available gates: %s",
gateFilter, a.availableGateNames(),
)
}

pCtx := &validate.PipelineContext{
Console: a.console,
Environment: a.env,
Project: a.projectConfig,
}

result, err := pipeline.Run(ctx, pCtx)
if err != nil {
return nil, err
}

// Display the validation report
report := &validate.ValidationReport{Result: result}
a.console.MessageUxItem(ctx, report)

// Format output for --output json
if a.formatter.Kind() == output.JsonFormat {
if err := a.formatter.Format(result, a.writer, nil); err != nil {
return nil, fmt.Errorf("formatting output: %w", err)
}
}

if result.HasErrors() {
return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: fmt.Sprintf(
"Validation found %d error(s) and %d warning(s).",
result.TotalErrors(), result.TotalWarnings(),
),
},
}, nil
}

return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: "Validation completed successfully.",
},
}, nil
}

// availableGateNames returns a comma-separated list of registered gate names.
func (a *ValidateAction) availableGateNames() string {
if len(a.gates) == 0 {
return "(none registered)"
}
names := make([]string, len(a.gates))
for i, g := range a.gates {
names[i] = g.Name()
}
return fmt.Sprintf("%v", names)
}
11 changes: 11 additions & 0 deletions cli/azd/internal/grpcserver/external_provisioning_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ func (p *ExternalProvisioningProvider) Destroy(
}, nil
}

// Validate is not yet supported for extension provisioning providers.
// Returns a skipped result.
func (p *ExternalProvisioningProvider) Validate(
ctx context.Context,
) (*provisioning.ValidateResult, error) {
return &provisioning.ValidateResult{
Skipped: true,
SkipReason: "validation not yet supported for extension providers",
}, nil
}

// EnsureEnv ensures the environment is configured properly.
func (p *ExternalProvisioningProvider) EnsureEnv(ctx context.Context) error {
req := &azdext.ProvisioningMessage{
Expand Down
10 changes: 10 additions & 0 deletions cli/azd/pkg/devcenter/provision_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,16 @@ func (p *ProvisionProvider) Preview(ctx context.Context) (*provisioning.DeployPr
return nil, fmt.Errorf("preview is not supported for devcenter")
}

// Validate is not yet supported for Dev Center.
func (p *ProvisionProvider) Validate(
ctx context.Context,
) (*provisioning.ValidateResult, error) {
return &provisioning.ValidateResult{
Skipped: true,
SkipReason: "validation not yet supported for Dev Center provider",
}, nil
}

// Destroy destroys the environment by deleting the ADE environment
func (p *ProvisionProvider) Destroy(
ctx context.Context,
Expand Down
Loading