diff --git a/cli/azd/cmd/env.go b/cli/azd/cmd/env.go index 40bd177862f..f1f0458abdb 100644 --- a/cli/azd/cmd/env.go +++ b/cli/azd/cmd/env.go @@ -1151,12 +1151,19 @@ func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, err Title: fmt.Sprintf("Refreshing environment %s (azd env refresh)", ef.env.Name()), }) + // Initialize services, skipping any with unsupported hosts (e.g., extension-provided hosts + // that are not currently loaded). Env refresh only needs infrastructure outputs from Azure; + // services with unsupported hosts simply won't receive the environment-updated event. if err := ef.projectManager.Initialize(ctx, ef.projectConfig); err != nil { - return nil, err + if !hasUnsupportedHostError(err) { + return nil, err + } } if err := ef.projectManager.EnsureAllTools(ctx, ef.projectConfig, nil); err != nil { - return nil, err + if !hasUnsupportedHostError(err) { + return nil, err + } } infra, err := ef.importManager.ProjectInfrastructure(ctx, ef.projectConfig) @@ -1253,6 +1260,14 @@ func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, err }, nil } +// hasUnsupportedHostError reports whether err (or any error in its chain) is caused by +// an unsupported service host. This lets env refresh continue when extension-provided hosts +// (e.g., azure.ai.agent) are not loaded. +func hasUnsupportedHostError(err error) bool { + _, ok := errors.AsType[*project.UnsupportedServiceHostError](err) + return ok +} + func newEnvGetValuesFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *envGetValuesFlags { flags := &envGetValuesFlags{} flags.Bind(cmd.Flags(), global) diff --git a/cli/azd/cmd/env_test.go b/cli/azd/cmd/env_test.go index e0b5abd24a4..bcfd09b1061 100644 --- a/cli/azd/cmd/env_test.go +++ b/cli/azd/cmd/env_test.go @@ -4,8 +4,12 @@ package cmd import ( + "errors" + "fmt" "testing" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/pkg/project" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -170,3 +174,73 @@ func TestNewEnvConfigUnsetCmd(t *testing.T) { require.Equal(t, "unset ", cmd.Use) require.NotEmpty(t, cmd.Short) } + +func TestHasUnsupportedHostError(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "unrelated error", + err: assert.AnError, + expected: false, + }, + { + name: "direct UnsupportedServiceHostError", + err: &project.UnsupportedServiceHostError{ + Host: "azure.ai.agent", + ServiceName: "agent", + }, + expected: true, + }, + { + name: "wrapped UnsupportedServiceHostError", + err: fmt.Errorf("initializing service 'agent', %w", &project.UnsupportedServiceHostError{ + Host: "azure.ai.agent", + ServiceName: "agent", + }), + expected: true, + }, + { + name: "wrapped in ErrorWithSuggestion", + err: fmt.Errorf("getting service target: %w", &internal.ErrorWithSuggestion{ + Err: &project.UnsupportedServiceHostError{ + Host: "azure.ai.agent", + ServiceName: "agent", + }, + Suggestion: "install an extension", + }), + expected: true, + }, + { + name: "errors.Join with UnsupportedServiceHostError", + err: errors.Join( + fmt.Errorf("initializing service 'agent', %w", &project.UnsupportedServiceHostError{ + Host: "azure.ai.agent", + ServiceName: "agent", + }), + fmt.Errorf("initializing service 'other', %w", &project.UnsupportedServiceHostError{ + Host: "azure.ai.other", + ServiceName: "other", + }), + ), + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := hasUnsupportedHostError(tt.err) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/cli/azd/pkg/project/project_manager.go b/cli/azd/pkg/project/project_manager.go index 652f41f5722..cfe21808ebf 100644 --- a/cli/azd/pkg/project/project_manager.go +++ b/cli/azd/pkg/project/project_manager.go @@ -92,7 +92,9 @@ func NewProjectManager( } } -// Initializes the project and all child services defined within the project configuration +// Initializes the project and all child services defined within the project configuration. +// Services with unsupported hosts (e.g., extension-provided hosts that are not loaded) are skipped, +// and the resulting UnsupportedServiceHostError is returned after all other services are initialized. func (pm *projectManager) Initialize(ctx context.Context, projectConfig *ProjectConfig) error { servicesStable, err := pm.importManager.ServiceStable(ctx, projectConfig) if err != nil { @@ -106,12 +108,22 @@ func (pm *projectManager) Initialize(ctx context.Context, projectConfig *Project tracing.SetUsageAttributes(fields.ProjectServiceTargetsKey.StringSlice(serviceTargets)) + var unsupportedHostErrors []error for _, svc := range servicesStable { if err := pm.serviceManager.Initialize(ctx, svc); err != nil { - return fmt.Errorf("initializing service '%s', %w", svc.Name, err) + initErr := fmt.Errorf("initializing service '%s', %w", svc.Name, err) + if _, ok := errors.AsType[*UnsupportedServiceHostError](err); ok { + unsupportedHostErrors = append(unsupportedHostErrors, initErr) + continue + } + return initErr } } + if len(unsupportedHostErrors) > 0 { + return errors.Join(unsupportedHostErrors...) + } + return nil } @@ -155,6 +167,7 @@ func (pm *projectManager) EnsureAllTools( return err } + var unsupportedHostErrors []error for _, svc := range servicesStable { if serviceFilterFn != nil && !serviceFilterFn(svc) { continue @@ -162,6 +175,10 @@ func (pm *projectManager) EnsureAllTools( svcTools, err := pm.serviceManager.GetRequiredTools(ctx, svc) if err != nil { + if _, ok := errors.AsType[*UnsupportedServiceHostError](err); ok { + unsupportedHostErrors = append(unsupportedHostErrors, fmt.Errorf("getting service required tools: %w", err)) + continue + } return fmt.Errorf("getting service required tools: %w", err) } @@ -172,6 +189,10 @@ func (pm *projectManager) EnsureAllTools( return err } + if len(unsupportedHostErrors) > 0 { + return errors.Join(unsupportedHostErrors...) + } + return nil }