Skip to content

Commit c4fdb2c

Browse files
vhvb1989Copilot
andcommitted
Refactor policy preflight to use server-side checkPolicyRestrictions API
Replace the custom client-side policy rule parsing engine with the server-side checkPolicyRestrictions API (Microsoft.PolicyInsights). The check is now scoped to storage accounts and calls the API in parallel for each account. Key changes: - PolicyService now uses armpolicyinsights.PolicyRestrictionsClient instead of manually fetching/parsing policy assignments & definitions - checkLocalAuthPolicy renamed to checkStorageAccountPolicy, filters snapshot resources to Microsoft.Storage/storageAccounts only - Parallel API calls per storage account via sync.WaitGroup.Go - Warning messages now include policy reasons from the server response - Replaced armpolicy SDK dependency with armpolicyinsights - Rewrote tests with mock HTTP transport for the new API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 65a6512 commit c4fdb2c

File tree

9 files changed

+804
-1042
lines changed

9 files changed

+804
-1042
lines changed

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
"request": "launch",
2626
"mode": "debug",
2727
"program": "${workspaceFolder}/cli/azd",
28-
"args": "${input:cliArgs}",
28+
"args": "up",
2929
"console": "integratedTerminal",
30+
"cwd": "/home/vivazqu/workspace/build/debug/preflight-agentic"
3031
}
3132
],
3233
"inputs": [

cli/azd/.vscode/cspell.yaml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,12 +314,8 @@ overrides:
314314
- fakeazure
315315
- filename: pkg/azapi/policy_service.go
316316
words:
317-
- armpolicy
318-
- disablelocalauth
319-
- allowsharedkeyaccess
320-
- policydefinitions
321-
- policysetdefinitions
322-
- managementgroups
317+
- armpolicyinsights
318+
- policyinsights
323319
- filename: "{pkg/azapi/permissions.go,pkg/infra/provisioning/bicep/bicep_provider.go}"
324320
words:
325321
- ABAC
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
# Extension Flag Handling Design
2+
3+
> **Status**: Design summary of current implementation (not a prescriptive spec).
4+
> Extracted from the codebase as of March 2026 to document the existing behavior and
5+
> call out undefined or ambiguous scenarios.
6+
7+
## Overview
8+
9+
When a user runs an extension command (e.g. `azd myext --my-flag value -e dev`), flags
10+
must be split between two consumers:
11+
12+
| Consumer | Needs | Examples |
13+
|----------|-------|---------|
14+
| **azd host** | Global flags that control host behavior | `--debug`, `--cwd`, `-e`, `--no-prompt` |
15+
| **Extension process** | Extension-specific flags + positional args | `--my-flag value`, `--model gpt-4` |
16+
17+
Today there is **no explicit separator** (like `--`) between the two. Instead, azd uses
18+
a combination of early pre-parsing and Cobra's `DisableFlagParsing` to handle this.
19+
20+
## Current Implementation
21+
22+
### Phase 1: Early Pre-Parsing (`ParseGlobalFlags`)
23+
24+
**File**: `cmd/auto_install.go``ParseGlobalFlags()`
25+
26+
Before Cobra builds the command tree, `ParseGlobalFlags()` extracts known azd global
27+
flags from the raw `os.Args` using a pflag FlagSet with `ParseErrorsAllowlist{UnknownFlags: true}`.
28+
Unknown flags (extension flags) are silently ignored.
29+
30+
**Flags parsed in this phase:**
31+
32+
| Flag | Short | Stored in `GlobalCommandOptions` |
33+
|------|-------|----------------------------------|
34+
| `--cwd` | `-C` | `Cwd` |
35+
| `--debug` || `EnableDebugLogging` |
36+
| `--no-prompt` || `NoPrompt` |
37+
| `--trace-log-file` || *(read by telemetry system)* |
38+
| `--trace-log-url` || *(read by telemetry system)* |
39+
40+
**Not parsed here**: `-e` / `--environment` is currently absent from `CreateGlobalFlagSet()`.
41+
It is registered as a per-command flag on commands that need it (via `envFlag` binding),
42+
not as a global flag. This is the root cause of [#7034](https://github.com/Azure/azure-dev/issues/7034).
43+
44+
The result is stored in a `GlobalCommandOptions` singleton registered in the IoC container.
45+
46+
**Agent detection**: If `--no-prompt` was not explicitly set, `ParseGlobalFlags` also
47+
checks `agentdetect.IsRunningInAgent()` and auto-enables `NoPrompt` for AI coding agents.
48+
49+
### Phase 2: Cobra Command Registration
50+
51+
**File**: `cmd/extensions.go``bindExtension()`
52+
53+
Extension commands are registered with:
54+
55+
```go
56+
cmd := &cobra.Command{
57+
Use: lastPart,
58+
DisableFlagParsing: true,
59+
}
60+
```
61+
62+
`DisableFlagParsing: true` means Cobra will **not** parse any flags for these commands.
63+
All tokens after the command name are passed to the action handler as raw `args`.
64+
65+
**Consequence**: Cobra's persistent flags (including `-e`/`--environment` if registered)
66+
are not parsed for extension commands. Calls to `cmd.Flags().GetString("environment")`
67+
silently return `""`.
68+
69+
### Phase 3: Extension Action Execution
70+
71+
**File**: `cmd/extensions.go``extensionAction.Run()`
72+
73+
The action handler builds `InvokeOptions` from two sources:
74+
75+
1. **Cobra persistent flags** (`cmd.Flags().Get*`) — for `debug`, `cwd`, `environment`
76+
2. **`GlobalCommandOptions`** — for `NoPrompt` (because it includes agent detection)
77+
78+
```go
79+
debugEnabled, _ := a.cmd.Flags().GetBool("debug")
80+
cwd, _ := a.cmd.Flags().GetString("cwd")
81+
envName, _ := a.cmd.Flags().GetString("environment")
82+
83+
options := &extensions.InvokeOptions{
84+
Args: a.args, // ALL raw args (extension + global flags)
85+
Debug: debugEnabled,
86+
NoPrompt: a.globalOptions.NoPrompt,
87+
Cwd: cwd,
88+
Environment: envName,
89+
}
90+
```
91+
92+
**Problem**: Because `DisableFlagParsing: true`, `cmd.Flags().Get*` calls for `debug`,
93+
`cwd`, and `environment` return zero values. Only `NoPrompt` works correctly because it
94+
reads from `globalOptions` (populated in Phase 1).
95+
96+
### Phase 4: Environment Variable Propagation
97+
98+
**File**: `pkg/extensions/runner.go``Runner.Invoke()`
99+
100+
Global flag values from `InvokeOptions` are converted to environment variables before
101+
spawning the extension process:
102+
103+
| `InvokeOptions` field | Environment variable | Condition |
104+
|-----------------------|---------------------|-----------|
105+
| `Debug` | `AZD_DEBUG=true` | if true |
106+
| `NoPrompt` | `AZD_NO_PROMPT=true` | if true |
107+
| `Cwd` | `AZD_CWD=<value>` | if non-empty |
108+
| `Environment` | `AZD_ENVIRONMENT=<value>` | if non-empty |
109+
110+
The extension process also receives:
111+
- `AZD_SERVER=<host>:<port>` — gRPC server address
112+
- `AZD_ACCESS_TOKEN=<jwt>` — Authentication token
113+
- `TRACEPARENT=<value>` — W3C trace context (if tracing is active)
114+
- Environment variables from the resolved azd environment (`env.Environ()`)
115+
116+
### Phase 5: Middleware Extensions (Listener Path)
117+
118+
**File**: `cmd/middleware/extensions.go``ExtensionsMiddleware.Run()`
119+
120+
Lifecycle extensions (those with `lifecycle-events`, `service-target-provider`, or
121+
`framework-service-provider` capabilities) use a different execution path:
122+
123+
- Started with fixed args `["listen"]`
124+
- Read global flags from `m.options.Flags` (Cobra persistent flags from the *parent* command)
125+
- Also have access to `m.globalOptions`
126+
127+
This path works for standard azd commands (like `azd deploy`) where Cobra *does* parse
128+
persistent flags. The middleware invocation constructs `InvokeOptions` similarly.
129+
130+
## What Extensions Receive
131+
132+
When a user runs: `azd myext --my-flag value -e dev --debug`
133+
134+
The extension process gets:
135+
136+
| Channel | Content |
137+
|---------|---------|
138+
| **Command-line args** | `["--my-flag", "value", "-e", "dev", "--debug"]` — ALL tokens after the command name, including azd global flags |
139+
| **Environment variables** | `AZD_DEBUG`, `AZD_NO_PROMPT`, `AZD_CWD`, `AZD_ENVIRONMENT` — set only if their `InvokeOptions` field is non-empty |
140+
| **gRPC services** | Extension can call `EnvironmentService.GetCurrentEnvironment()` to get the resolved environment |
141+
142+
**Key observation**: Global azd flags appear in **both** the raw args and environment
143+
variables. Extensions must decide which source to trust.
144+
145+
## Extension-Side Flag Parsing
146+
147+
Extensions are expected to:
148+
149+
1. **Read global azd flags from environment variables** (`AZD_DEBUG`, `AZD_NO_PROMPT`,
150+
`AZD_CWD`, `AZD_ENVIRONMENT`), not from their command-line args.
151+
2. **Parse their own flags** from the command-line args they receive. The azd SDK provides
152+
helpers (`azdext` package) for this.
153+
3. **Ignore azd global flags in their args** — these are artifacts of `DisableFlagParsing`
154+
and should not be interpreted by the extension.
155+
156+
However, this contract is **not formally documented or enforced**. Extensions using the
157+
Go SDK with Cobra will likely re-parse all args including the azd global flags.
158+
159+
## Undefined Scenarios and Edge Cases
160+
161+
### 1. Flag Name Collisions
162+
163+
**Scenario**: Extension defines a flag with the same name as an azd global flag
164+
(e.g., `--debug` meaning "debug output format" in the extension).
165+
166+
**Current behavior**: `ParseGlobalFlags` will consume `--debug` and set
167+
`GlobalCommandOptions.EnableDebugLogging = true`. The extension will also see `--debug`
168+
in its raw args and may interpret it differently.
169+
170+
**Impact**: Both azd and the extension act on the same flag with different semantics.
171+
There is no way to say "this `--debug` is for the extension, not azd".
172+
173+
### 2. Global Flags Not Stripped From Extension Args
174+
175+
**Scenario**: User runs `azd myext -e dev --debug --my-flag value`.
176+
177+
**Current behavior**: Extension receives args `["-e", "dev", "--debug", "--my-flag", "value"]`.
178+
The azd global flags are **not** removed from the args before passing to the extension.
179+
180+
**Impact**: If the extension uses a strict flag parser, unknown flags like `--debug` may
181+
cause parse errors. Extensions must use permissive parsing or explicitly handle azd flags.
182+
183+
### 3. No `--` Separator Convention
184+
185+
**Scenario**: User wants to pass `--debug` to the extension but not to azd.
186+
187+
**Current behavior**: No mechanism exists. `ParseGlobalFlags` will always consume
188+
recognized flags regardless of position.
189+
190+
**Potential design**: A `--` separator could be introduced:
191+
`azd --debug myext -- --debug` (first `--debug` for azd, second for extension).
192+
This is not implemented.
193+
194+
### 4. `-e` / `--environment` Not Pre-Parsed
195+
196+
**Scenario**: User runs `azd myext -e dev`.
197+
198+
**Current behavior** (main branch): `-e` is NOT in `CreateGlobalFlagSet()`, so
199+
`ParseGlobalFlags` ignores it. `cmd.Flags().GetString("environment")` returns `""`
200+
due to `DisableFlagParsing`. Result: `AZD_ENVIRONMENT` is never set, and `lazyEnv`
201+
resolves to the default environment.
202+
203+
**Impact**: This is the bug described in [#7034](https://github.com/Azure/azure-dev/issues/7034).
204+
The default environment's variables leak into the extension process.
205+
206+
### 5. Environment Variable Injection From Default Environment
207+
208+
**Scenario**: Extension commands inject `lazyEnv.Environ()` (the resolved azd
209+
environment's key-value pairs) into the extension process's environment.
210+
211+
**Question**: Should extensions receive environment variables from the azd environment
212+
at all, or should they exclusively use the gRPC `EnvironmentService`?
213+
214+
**Current behavior**: `extensionAction.Run()` calls `a.lazyEnv.GetValue()` and appends
215+
all key-value pairs as process environment variables. This means the extension inherits
216+
every value from the resolved azd environment.
217+
218+
**Trade-off**: Injecting env vars is convenient (extensions "just work" with standard
219+
`os.Getenv`), but it creates an implicit coupling and makes it hard to distinguish
220+
azd-injected variables from system environment variables.
221+
222+
### 6. `--cwd` Semantics
223+
224+
**Scenario**: User runs `azd myext -C ./subdir --my-flag value`.
225+
226+
**Current behavior**: `ParseGlobalFlags` captures `Cwd = "./subdir"`. The root command
227+
changes the process working directory before the extension runs. `AZD_CWD=./subdir` is
228+
set in the extension's environment.
229+
230+
**Question**: Should the extension receive the *original* cwd or the *resolved* cwd?
231+
Currently it receives the raw flag value (potentially relative), but the process has
232+
already `cd`'d into that directory.
233+
234+
### 7. Middleware vs Command Extension Flag Parity
235+
236+
**Scenario**: A lifecycle extension (middleware path) needs the `-e` environment name.
237+
238+
**Current behavior**: Middleware reads `m.options.Flags.GetString("environment")` from
239+
Cobra persistent flags of the parent command (e.g., `azd deploy`). This works because
240+
the parent command *does* parse flags.
241+
242+
**Gap**: The two execution paths (command extensions vs middleware extensions) have
243+
different flag resolution strategies. Command extensions cannot rely on Cobra flags;
244+
middleware extensions can.
245+
246+
### 8. Help Text for Extension Commands
247+
248+
**Scenario**: User runs `azd myext --help`.
249+
250+
**Current behavior**: Because `DisableFlagParsing: true`, Cobra does not intercept
251+
`--help`. The `--help` flag is passed as an arg to the extension. The extension must
252+
handle it. Cobra's auto-generated help (which would show global flags) is not displayed.
253+
254+
**Impact**: Users don't see a consistent help experience between azd commands and
255+
extension commands. Global flags like `-e`, `--debug` are not listed in extension help.
256+
257+
## Recommendations
258+
259+
These are observations about gaps in the current design, not prescriptive changes:
260+
261+
1. **Document the flag contract**: Extensions should know definitively whether to read
262+
global flags from env vars or args. Today this is implicit.
263+
264+
2. **Consider stripping global flags from extension args**: If azd globals are communicated
265+
via env vars, they arguably should not appear in the extension's command-line args.
266+
This would eliminate collision issues. However, this would be a breaking change for
267+
extensions that currently parse `-e` from their args.
268+
269+
3. **Consider a `--` separator**: `azd [azd-flags] myext -- [ext-flags]` would make the
270+
boundary explicit. This is a convention used by many CLI tools (npm, cargo, kubectl).
271+
272+
4. **Unify flag resolution**: The divergence between command extensions (can't use Cobra
273+
flags) and middleware extensions (can use Cobra flags) is a source of bugs. A single
274+
resolution path through `GlobalCommandOptions` would be more robust.
275+
276+
5. **Document `AZD_ENVIRONMENT`, `AZD_DEBUG`, `AZD_NO_PROMPT`, `AZD_CWD`**: These
277+
environment variables are set for extension processes but not listed in
278+
`docs/environment-variables.md`.
279+
280+
## File Reference
281+
282+
| File | Role |
283+
|------|------|
284+
| `cmd/auto_install.go` | `CreateGlobalFlagSet()`, `ParseGlobalFlags()` |
285+
| `cmd/extensions.go` | `bindExtension()` (DisableFlagParsing), `extensionAction.Run()` |
286+
| `cmd/container.go` | `EnvFlag` DI resolver (environment name resolution chain) |
287+
| `cmd/middleware/extensions.go` | Lifecycle extension invocation (middleware path) |
288+
| `cmd/root.go` | Command tree construction, global flag registration |
289+
| `pkg/extensions/runner.go` | `InvokeOptions`, env var propagation, process spawning |
290+
| `internal/global_command_options.go` | `GlobalCommandOptions` struct |
291+
| `internal/env_flag.go` | `EnvFlag` type, `EnvironmentNameFlagName` constant |

cli/azd/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ require (
2020
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/machinelearning/armmachinelearning/v3 v3.2.0
2121
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0
2222
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights/v2 v2.0.2
23+
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/policyinsights/armpolicyinsights v0.9.0
2324
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0
2425
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks v1.0.1
25-
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armpolicy v0.10.0
2626
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
2727
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0
2828
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.7

cli/azd/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 h1:L7G3d
4545
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0/go.mod h1:Ms6gYEy0+A2knfKrwdatsggTXYA2+ICKug8w7STorFw=
4646
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights/v2 v2.0.2 h1:SFLbQmpdytToYZQJw5NqrZRwHPIGJmf5ZgjStbLfUuU=
4747
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights/v2 v2.0.2/go.mod h1:H3EFkhcVTisidszwtIkRDggjS2HmOIA26J3g8hDdHAY=
48+
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/policyinsights/armpolicyinsights v0.9.0 h1:VK9yyk+hLSM+9UHsemlsON7sqQqbCu9O349e0RG8kBg=
49+
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/policyinsights/armpolicyinsights v0.9.0/go.mod h1:FFXnZdOg5Gt9/Pfljm1IzcezNfmbm2ScWYT5KH7KiNc=
4850
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk=
4951
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
5052
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks v1.0.1 h1:bcgO/crpp7wqI0Froi/I4C2fme7Vk/WLusbV399Do8I=
5153
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks v1.0.1/go.mod h1:kvfPmsE8gpOwwC1qrO1FeyBDDNfnwBN5UU3MPNiWW7I=
52-
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armpolicy v0.10.0 h1:FCprRw2Uzske3FiFVGm6MqJY829zrAJLiN4coFueWis=
53-
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armpolicy v0.10.0/go.mod h1:koK4/Mf6lxFkYavGzZnzTUOEmY8ic9tN44UmWZsGfrk=
5454
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
5555
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
5656
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY=

0 commit comments

Comments
 (0)