diff --git a/cli/azd/pkg/project/project_manager.go b/cli/azd/pkg/project/project_manager.go index e939f3da5f9..652f41f5722 100644 --- a/cli/azd/pkg/project/project_manager.go +++ b/cli/azd/pkg/project/project_manager.go @@ -8,7 +8,10 @@ import ( "errors" "fmt" "os" + "slices" + "strings" + "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/internal/tracing" "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" @@ -205,6 +208,12 @@ func (pm *projectManager) EnsureFrameworkTools( return nil } +// svcToolInfo tracks whether a service's target required Docker. +type svcToolInfo struct { + svc *ServiceConfig + needsDocker bool +} + func (pm *projectManager) EnsureServiceTargetTools( ctx context.Context, projectConfig *ProjectConfig, @@ -217,6 +226,8 @@ func (pm *projectManager) EnsureServiceTargetTools( return err } + var svcTools []svcToolInfo + for _, svc := range servicesStable { if serviceFilterFn != nil && !serviceFilterFn(svc) { continue @@ -227,17 +238,83 @@ func (pm *projectManager) EnsureServiceTargetTools( return fmt.Errorf("getting service target: %w", err) } - serviceTargetTools := serviceTarget.RequiredExternalTools(ctx, svc) - requiredTools = append(requiredTools, serviceTargetTools...) + targetTools := serviceTarget.RequiredExternalTools(ctx, svc) + requiredTools = append(requiredTools, targetTools...) + + needsDocker := false + for _, tool := range targetTools { + if tool.Name() == "Docker" { + needsDocker = true + break + } + } + svcTools = append(svcTools, svcToolInfo{svc: svc, needsDocker: needsDocker}) } if err := tools.EnsureInstalled(ctx, tools.Unique(requiredTools)...); err != nil { + if toolErr, ok := errors.AsType[*tools.MissingToolErrors](err); ok { + if suggestion := suggestRemoteBuild(svcTools, toolErr); suggestion != nil { + return suggestion + } + } return err } return nil } +// suggestRemoteBuild checks if Docker is in the missing tools list and whether any +// services that required it could use remote builds instead. Only services whose +// service target actually listed Docker as required are included in the suggestion. +func suggestRemoteBuild( + svcTools []svcToolInfo, + toolErr *tools.MissingToolErrors, +) *internal.ErrorWithSuggestion { + if !slices.Contains(toolErr.ToolNames, "Docker") { + return nil + } + + // Find services that actually required Docker (per their service target) + // and could use remoteBuild instead. + var remoteBuildCapable []string + for _, info := range svcTools { + if !info.needsDocker { + continue + } + remoteBuildCapable = append(remoteBuildCapable, info.svc.Name) + } + + if len(remoteBuildCapable) == 0 { + return nil + } + + // Check whether the container runtime is not installed or just not running + errMsg := toolErr.Error() + isNotRunning := strings.Contains(errMsg, "is not running") + + serviceList := strings.Join(remoteBuildCapable, ", ") + var suggestion string + if isNotRunning { + suggestion = fmt.Sprintf( + "Services [%s] can build on Azure instead of locally.\n"+ + "Set 'remoteBuild: true' under the 'docker:' section for each service in azure.yaml,\n"+ + "or start your container runtime (Docker/Podman).", + serviceList) + } else { + suggestion = fmt.Sprintf( + "Services [%s] can build on Azure instead of locally.\n"+ + "Set 'remoteBuild: true' under the 'docker:' section for each service in azure.yaml,\n"+ + "or install Docker (https://aka.ms/azure-dev/docker-install)\n"+ + "or Podman (https://aka.ms/azure-dev/podman-install).", + serviceList) + } + + return &internal.ErrorWithSuggestion{ + Err: toolErr, + Suggestion: suggestion, + } +} + func (pm *projectManager) EnsureRestoreTools( ctx context.Context, projectConfig *ProjectConfig, diff --git a/cli/azd/pkg/project/project_manager_test.go b/cli/azd/pkg/project/project_manager_test.go new file mode 100644 index 00000000000..89d98b00175 --- /dev/null +++ b/cli/azd/pkg/project/project_manager_test.go @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package project + +import ( + "fmt" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/tools" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_suggestRemoteBuild(t *testing.T) { + dockerMissing := &tools.MissingToolErrors{ + ToolNames: []string{"Docker"}, + Errs: []error{fmt.Errorf("neither docker nor podman is installed")}, + } + dockerNotRunning := &tools.MissingToolErrors{ + ToolNames: []string{"Docker"}, + Errs: []error{fmt.Errorf("the Docker service is not running, please start it")}, + } + bicepMissing := &tools.MissingToolErrors{ + ToolNames: []string{"bicep"}, + Errs: []error{assert.AnError}, + } + + tests := []struct { + name string + svcTools []svcToolInfo + toolErr *tools.MissingToolErrors + wantSuggestion bool + wantContains string + }{ + { + name: "Service_needing_Docker_suggests", + svcTools: []svcToolInfo{ + {svc: &ServiceConfig{Name: "api"}, needsDocker: true}, + }, + toolErr: dockerMissing, + wantSuggestion: true, + wantContains: "api", + }, + { + name: "Multiple_services_lists_all", + svcTools: []svcToolInfo{ + {svc: &ServiceConfig{Name: "api"}, needsDocker: true}, + {svc: &ServiceConfig{Name: "web"}, needsDocker: true}, + }, + toolErr: dockerMissing, + wantSuggestion: true, + wantContains: "api, web", + }, + { + name: "Service_not_needing_Docker_no_suggestion", + svcTools: []svcToolInfo{ + {svc: &ServiceConfig{Name: "api"}, needsDocker: false}, + }, + toolErr: dockerMissing, + wantSuggestion: false, + }, + { + name: "Non_Docker_tool_missing_no_suggestion", + svcTools: []svcToolInfo{ + {svc: &ServiceConfig{Name: "api"}, needsDocker: true}, + }, + toolErr: bicepMissing, + wantSuggestion: false, + }, + { + name: "Mixed_services_only_Docker_ones", + svcTools: []svcToolInfo{ + {svc: &ServiceConfig{Name: "api"}, needsDocker: true}, + {svc: &ServiceConfig{Name: "web"}, needsDocker: false}, + {svc: &ServiceConfig{Name: "worker"}, needsDocker: true}, + }, + toolErr: dockerMissing, + wantSuggestion: true, + wantContains: "api, worker", + }, + { + name: "Docker_not_running_suggests_start", + svcTools: []svcToolInfo{ + {svc: &ServiceConfig{Name: "api"}, needsDocker: true}, + }, + toolErr: dockerNotRunning, + wantSuggestion: true, + wantContains: "start your container runtime", + }, + { + name: "Docker_not_installed_suggests_install", + svcTools: []svcToolInfo{ + {svc: &ServiceConfig{Name: "api"}, needsDocker: true}, + }, + toolErr: dockerMissing, + wantSuggestion: true, + wantContains: "install Docker", + }, + { + name: "Empty_services_no_suggestion", + svcTools: []svcToolInfo{}, + toolErr: dockerMissing, + wantSuggestion: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := suggestRemoteBuild(tt.svcTools, tt.toolErr) + + if !tt.wantSuggestion { + assert.Nil(t, result) + return + } + + require.NotNil(t, result) + assert.Contains(t, result.Suggestion, tt.wantContains) + assert.Contains(t, result.Suggestion, "remoteBuild") + }) + } +} diff --git a/cli/azd/resources/error_suggestions.yaml b/cli/azd/resources/error_suggestions.yaml index 72d0c2ebc6a..5dc30847f2f 100644 --- a/cli/azd/resources/error_suggestions.yaml +++ b/cli/azd/resources/error_suggestions.yaml @@ -479,6 +479,34 @@ rules: - url: "https://learn.microsoft.com/azure/developer/azure-developer-cli/reference#azd-auth-login" title: "azd auth login reference" + # ============================================================================ + # Tool Missing Errors — Docker / Container Runtime + # ============================================================================ + + - errorType: "MissingToolErrors" + patterns: + - "is not running" + message: "The container runtime (Docker/Podman) is not running." + suggestion: >- + Start your container runtime, or build on Azure instead by setting + 'remoteBuild: true' under the 'docker:' section for each service in azure.yaml. + + - errorType: "MissingToolErrors" + patterns: + - "Docker" + message: "No container runtime (Docker/Podman) is installed." + suggestion: >- + If your services use Container Apps or AKS, you can build on Azure instead + of locally. Set 'remoteBuild: true' under the 'docker:' section for each + service in azure.yaml. Otherwise, install Docker or Podman. + links: + - url: "https://aka.ms/azure-dev/docker-install" + title: "Install Docker" + - url: "https://aka.ms/azure-dev/podman-install" + title: "Install Podman" + - url: "https://learn.microsoft.com/azure/developer/azure-developer-cli/azd-schema" + title: "azure.yaml schema reference" + # ============================================================================ # Text Pattern Rules — Specific patterns first # These are fallbacks for errors without typed Go structs.