Skip to content

Commit d4f76b8

Browse files
authored
azd env refresh with extension-provided hosts and provisioning providers (#9017)
* fix: support extension-provided hosts and providers in azd env refresh env refresh now initializes only framework services (best-effort, never service targets) and starts an extension-provided provisioning provider on demand, so projects using hosts/providers like azure.ai.agent and microsoft.foundry can refresh outputs. Missing deployments are reported informationally instead of failing.
1 parent 830da92 commit d4f76b8

17 files changed

Lines changed: 1223 additions & 113 deletions

cli/azd/cmd/container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
957957
container.MustRegisterSingleton(extensions.NewManager)
958958
container.MustRegisterSingleton(extensions.NewSourceManager)
959959
container.MustRegisterSingleton(extensions.NewRunner)
960+
container.MustRegisterScoped(middleware.NewExtensionActivator)
960961
container.MustRegisterSingleton(func(serviceLocator ioc.ServiceLocator) *lazy.Lazy[*extensions.Runner] {
961962
return lazy.NewLazy(func() (*extensions.Runner, error) {
962963
var runner *extensions.Runner

cli/azd/cmd/container_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,3 +638,18 @@ func Test_ResolveAction_WithNilMiddleware(t *testing.T) {
638638
_, err := resolveAction[*coverageTestAction](container, "test-action")
639639
require.Error(t, err) // not registered
640640
}
641+
642+
// Verifies the ExtensionActivator used by env refresh resolves from the IoC container.
643+
func Test_Resolve_ExtensionActivator(t *testing.T) {
644+
t.Parallel()
645+
container := ioc.NewNestedContainer(nil)
646+
ioc.RegisterInstance(container, t.Context())
647+
ioc.RegisterInstance(container, &internal.GlobalCommandOptions{})
648+
ioc.RegisterInstance(container, &cobra.Command{})
649+
registerCommonDependencies(container)
650+
651+
var activator *middleware.ExtensionActivator
652+
err := container.Resolve(&activator)
653+
require.NoError(t, err)
654+
require.NotNil(t, activator)
655+
}

cli/azd/cmd/env.go

Lines changed: 122 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
1818
"github.com/azure/azure-dev/cli/azd/cmd/actions"
19+
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
1920
"github.com/azure/azure-dev/cli/azd/internal"
2021
"github.com/azure/azure-dev/cli/azd/internal/tracing"
2122
"github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
@@ -26,9 +27,11 @@ import (
2627
"github.com/azure/azure-dev/cli/azd/pkg/entraid"
2728
"github.com/azure/azure-dev/cli/azd/pkg/environment"
2829
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
30+
"github.com/azure/azure-dev/cli/azd/pkg/infra"
2931
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
3032
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning/bicep"
3133
"github.com/azure/azure-dev/cli/azd/pkg/input"
34+
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
3235
"github.com/azure/azure-dev/cli/azd/pkg/keyvault"
3336
"github.com/azure/azure-dev/cli/azd/pkg/output"
3437
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
@@ -1100,10 +1103,18 @@ func newEnvRefreshCmd() *cobra.Command {
11001103
return cmd
11011104
}
11021105

1106+
// provisioningProviderActivator is the narrow slice of *middleware.ExtensionActivator that env
1107+
// refresh consumes; it exists so tests can stub extension activation.
1108+
type provisioningProviderActivator interface {
1109+
EnsureProvisioningProviders(ctx context.Context, providerNames []string, environmentName string) (func(), error)
1110+
SuggestExtensionForProvider(ctx context.Context, providerName string) string
1111+
}
1112+
11031113
type envRefreshAction struct {
11041114
provisionManager *provisioning.Manager
11051115
projectConfig *project.ProjectConfig
11061116
projectManager project.ProjectManager
1117+
extensionActivator provisioningProviderActivator
11071118
env *environment.Environment
11081119
envManager environment.Manager
11091120
prompters prompt.Prompter
@@ -1119,6 +1130,7 @@ func newEnvRefreshAction(
11191130
provisionManager *provisioning.Manager,
11201131
projectConfig *project.ProjectConfig,
11211132
projectManager project.ProjectManager,
1133+
extensionActivator *middleware.ExtensionActivator,
11221134
env *environment.Environment,
11231135
envManager environment.Manager,
11241136
prompters prompt.Prompter,
@@ -1132,6 +1144,7 @@ func newEnvRefreshAction(
11321144
return &envRefreshAction{
11331145
provisionManager: provisionManager,
11341146
projectManager: projectManager,
1147+
extensionActivator: extensionActivator,
11351148
env: env,
11361149
envManager: envManager,
11371150
prompters: prompters,
@@ -1145,35 +1158,61 @@ func newEnvRefreshAction(
11451158
}
11461159
}
11471160

1161+
// suggestionForUnresolvedProvider returns the id of a registry extension to suggest installing
1162+
// when err is a provider-resolution failure; any other initialization error skips the registry
1163+
// lookup and returns an empty string.
1164+
func suggestionForUnresolvedProvider(
1165+
ctx context.Context,
1166+
err error,
1167+
activator provisioningProviderActivator,
1168+
providerName string,
1169+
) string {
1170+
if !errors.Is(err, ioc.ErrResolveInstance) {
1171+
return ""
1172+
}
1173+
1174+
return activator.SuggestExtensionForProvider(ctx, providerName)
1175+
}
1176+
11481177
func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, error) {
11491178
// Command title
11501179
ef.console.MessageUxItem(ctx, &ux.MessageTitle{
11511180
Title: fmt.Sprintf("Refreshing environment %s (azd env refresh)", ef.env.Name()),
11521181
})
11531182

1154-
if err := ef.projectManager.Initialize(ctx, ef.projectConfig); err != nil {
1155-
return nil, err
1156-
}
1157-
1158-
if err := ef.projectManager.EnsureAllTools(ctx, ef.projectConfig, nil); err != nil {
1159-
return nil, err
1160-
}
1161-
1162-
infra, err := ef.importManager.ProjectInfrastructure(ctx, ef.projectConfig)
1183+
// `env refresh` is read-only: it deliberately skips service-target initialization and tool
1184+
// checks, which would fail for extension-provided hosts (for example `azure.ai.agent`).
1185+
// Framework lifecycle hooks are wired best-effort after the outputs are pulled - see below.
1186+
projectInfra, err := ef.importManager.ProjectInfrastructure(ctx, ef.projectConfig)
11631187
if err != nil {
11641188
return nil, err
11651189
}
1166-
defer func() { _ = infra.Cleanup() }()
1190+
defer func() { _ = projectInfra.Cleanup() }()
11671191

1168-
layers := infra.Options.GetLayers()
1192+
layers := projectInfra.Options.GetLayers()
11691193
if ef.flags.layer != "" {
1170-
layerOpt, err := infra.Options.GetLayer(ef.flags.layer)
1194+
layerOpt, err := projectInfra.Options.GetLayer(ef.flags.layer)
11711195
if err != nil {
11721196
return nil, err
11731197
}
11741198
layers = []provisioning.Options{layerOpt}
11751199
}
11761200

1201+
// Extension-provided provisioning providers (for example `microsoft.foundry`) are only
1202+
// resolvable while the owning extension runs, and `env refresh` does not run the extensions
1203+
// middleware. Start just the installed extension(s) declaring the configured provider(s);
1204+
// all other names resolve natively as in every other command.
1205+
providerNames := make([]string, 0, len(layers))
1206+
for _, layer := range layers {
1207+
providerNames = append(providerNames, string(layer.Provider))
1208+
}
1209+
1210+
cleanupProviders, err := ef.extensionActivator.EnsureProvisioningProviders(ctx, providerNames, ef.env.Name())
1211+
if err != nil {
1212+
return nil, fmt.Errorf("activating provisioning provider extensions: %w", err)
1213+
}
1214+
defer cleanupProviders()
1215+
11771216
// If resource group is defined within the project but not in the environment then
11781217
// add it to the environment to support BYOI lookup scenarios like ADE
11791218
// Infra providers do not currently have access to project configuration
@@ -1183,6 +1222,7 @@ func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, err
11831222
}
11841223

11851224
var state provisioning.State
1225+
stateRefreshed := false
11861226
for _, layer := range layers {
11871227
if ef.flags.layer != "" || len(layers) > 1 {
11881228
ef.console.EnsureBlankLine(ctx)
@@ -1200,20 +1240,62 @@ func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, err
12001240
return nil, err
12011241
}
12021242
} else if err != nil {
1203-
return nil, fmt.Errorf("initializing provisioning manager: %w", err)
1243+
err = fmt.Errorf("initializing provisioning manager: %w", err)
1244+
1245+
// A resolution failure for a provider that a registry (non-installed) extension
1246+
// declares is almost certainly the missing extension: suggest installing it.
1247+
if extensionId := suggestionForUnresolvedProvider(
1248+
ctx, err, ef.extensionActivator, string(layer.Provider)); extensionId != "" {
1249+
return nil, &internal.ErrorWithSuggestion{
1250+
Err: err,
1251+
Suggestion: fmt.Sprintf(
1252+
"Provisioning provider '%s' is supplied by the '%s' extension. To install it, run %s",
1253+
layer.Provider,
1254+
extensionId,
1255+
output.WithHighLightFormat("azd extension install %s", extensionId),
1256+
),
1257+
}
1258+
}
1259+
1260+
return nil, err
12041261
}
12051262

12061263
stateOptions := provisioning.NewStateOptions(ef.flags.hint)
12071264
result, err := ef.provisionManager.State(ctx, stateOptions)
12081265
if err != nil {
1266+
// No deployment exists yet (for example, refresh before `azd provision`): this is
1267+
// informational, not an error - continue so any other layers still refresh. An
1268+
// explicit --hint stays a hard error because CompletedDeployments wraps the same
1269+
// sentinel when deployments exist but none matches the hint.
1270+
if errors.Is(err, infra.ErrDeploymentsNotFound) && ef.flags.hint == "" {
1271+
ef.console.Message(ctx, fmt.Sprintf(
1272+
"No deployment was found for environment '%s'; there are no outputs to refresh yet. "+
1273+
"Run %s to create one.",
1274+
ef.env.Name(),
1275+
output.WithHighLightFormat("azd provision"),
1276+
))
1277+
continue
1278+
}
1279+
12091280
return nil, fmt.Errorf("getting deployment: %w", err)
12101281
}
12111282

1283+
// Extension providers may reply with an empty state result (nothing deployed yet); treat
1284+
// it like the no-deployment case rather than dereferencing a nil state.
1285+
if result == nil || result.State == nil {
1286+
ef.console.Message(ctx, fmt.Sprintf(
1287+
"No deployment state was found for environment '%s'; there are no outputs to refresh yet.",
1288+
ef.env.Name(),
1289+
))
1290+
continue
1291+
}
1292+
12121293
if err := provisioning.UpdateEnvironment(ctx, result.State.Outputs, ef.env, ef.envManager); err != nil {
12131294
return nil, err
12141295
}
12151296

12161297
state.MergeInto(*result.State)
1298+
stateRefreshed = true
12171299
}
12181300

12191301
if ef.formatter.Kind() == output.JsonFormat {
@@ -1223,23 +1305,36 @@ func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, err
12231305
}
12241306
}
12251307

1226-
servicesStable, err := ef.importManager.ServiceStable(ctx, ef.projectConfig)
1227-
if err != nil {
1228-
return nil, err
1229-
}
1308+
// Wire framework lifecycle hooks (such as the .NET ServiceEventEnvUpdated handler that syncs
1309+
// outputs into user-secrets) best-effort per service, and raise the event only when a layer
1310+
// actually produced state. Services whose framework fails to initialize are reported and
1311+
// skipped rather than failing the refresh.
1312+
if stateRefreshed {
1313+
initializedServices, skipped, err := ef.projectManager.InitializeFrameworks(ctx, ef.projectConfig)
1314+
if err != nil {
1315+
return nil, err
1316+
}
12301317

1231-
for _, svc := range servicesStable {
1232-
eventArgs := project.ServiceLifecycleEventArgs{
1233-
Project: ef.projectConfig,
1234-
Service: svc,
1235-
ServiceContext: project.NewServiceContext(),
1236-
Args: map[string]any{
1237-
"bicepOutput": state.Outputs,
1238-
},
1318+
for _, skip := range skipped {
1319+
ef.console.MessageUxItem(ctx, &ux.WarningMessage{
1320+
Description: fmt.Sprintf(
1321+
"Skipping environment update events for service '%s': %v", skip.Service.Name, skip.Err),
1322+
})
12391323
}
12401324

1241-
if err := svc.RaiseEvent(ctx, project.ServiceEventEnvUpdated, eventArgs); err != nil {
1242-
return nil, err
1325+
for _, svc := range initializedServices {
1326+
eventArgs := project.ServiceLifecycleEventArgs{
1327+
Project: ef.projectConfig,
1328+
Service: svc,
1329+
ServiceContext: project.NewServiceContext(),
1330+
Args: map[string]any{
1331+
"bicepOutput": state.Outputs,
1332+
},
1333+
}
1334+
1335+
if err := svc.RaiseEvent(ctx, project.ServiceEventEnvUpdated, eventArgs); err != nil {
1336+
return nil, err
1337+
}
12431338
}
12441339
}
12451340

0 commit comments

Comments
 (0)