Skip to content

Commit b2b1edf

Browse files
spboyerCopilot
andauthored
Suggest remoteBuild when Docker is missing (#7247)
* Suggest remoteBuild when Docker is missing (#7240, #5715) When Docker is not installed or not running, azd now suggests 'remoteBuild: true' as an alternative for services that support it (Container Apps, AKS). This addresses 2,936 Docker.missing failures and 4,655 Docker.failed errors per 28 days. Two-layer approach: 1. error_suggestions.yaml: Added catch-all rule matching MissingToolErrors with 'Docker' pattern, suggesting remoteBuild with install links. Covers all error paths as a safety net. 2. EnsureServiceTargetTools: Enhanced with service-aware suggestion. When Docker is missing, inspects which services actually need it (excluding dotnet-publish services that bypass Docker) and returns a targeted ErrorWithSuggestion listing the specific services that can use remoteBuild. Fixes #7240 Fixes #5715 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review: add Podman install link to Docker suggestion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review: differentiate not-installed vs not-running, add Podman Per @weikanglim feedback: - Distinguish 'not installed' from 'not running' with different suggestions (start runtime vs install Docker/Podman) - Mention Podman alongside Docker in both Go suggestion and YAML rules - Split YAML rule into two: 'not running' (matched first) and 'not installed' - Add test cases for both scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix line length lint (125 char max) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor: use service target tool requirements instead of host assumptions Per @vhvb1989 feedback: project_manager should not assume which targets support remoteBuild. Instead, check whether the service target's RequiredExternalTools actually listed Docker, which respects custom service targets from extensions. The suggestRemoteBuild helper now takes svcToolInfo (which services needed Docker per their target) instead of checking Host.RequiresContainer(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6467e1c commit b2b1edf

File tree

3 files changed

+229
-2
lines changed

3 files changed

+229
-2
lines changed

cli/azd/pkg/project/project_manager.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88
"errors"
99
"fmt"
1010
"os"
11+
"slices"
12+
"strings"
1113

14+
"github.com/azure/azure-dev/cli/azd/internal"
1215
"github.com/azure/azure-dev/cli/azd/internal/tracing"
1316
"github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
1417
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
@@ -205,6 +208,12 @@ func (pm *projectManager) EnsureFrameworkTools(
205208
return nil
206209
}
207210

211+
// svcToolInfo tracks whether a service's target required Docker.
212+
type svcToolInfo struct {
213+
svc *ServiceConfig
214+
needsDocker bool
215+
}
216+
208217
func (pm *projectManager) EnsureServiceTargetTools(
209218
ctx context.Context,
210219
projectConfig *ProjectConfig,
@@ -217,6 +226,8 @@ func (pm *projectManager) EnsureServiceTargetTools(
217226
return err
218227
}
219228

229+
var svcTools []svcToolInfo
230+
220231
for _, svc := range servicesStable {
221232
if serviceFilterFn != nil && !serviceFilterFn(svc) {
222233
continue
@@ -227,17 +238,83 @@ func (pm *projectManager) EnsureServiceTargetTools(
227238
return fmt.Errorf("getting service target: %w", err)
228239
}
229240

230-
serviceTargetTools := serviceTarget.RequiredExternalTools(ctx, svc)
231-
requiredTools = append(requiredTools, serviceTargetTools...)
241+
targetTools := serviceTarget.RequiredExternalTools(ctx, svc)
242+
requiredTools = append(requiredTools, targetTools...)
243+
244+
needsDocker := false
245+
for _, tool := range targetTools {
246+
if tool.Name() == "Docker" {
247+
needsDocker = true
248+
break
249+
}
250+
}
251+
svcTools = append(svcTools, svcToolInfo{svc: svc, needsDocker: needsDocker})
232252
}
233253

234254
if err := tools.EnsureInstalled(ctx, tools.Unique(requiredTools)...); err != nil {
255+
if toolErr, ok := errors.AsType[*tools.MissingToolErrors](err); ok {
256+
if suggestion := suggestRemoteBuild(svcTools, toolErr); suggestion != nil {
257+
return suggestion
258+
}
259+
}
235260
return err
236261
}
237262

238263
return nil
239264
}
240265

266+
// suggestRemoteBuild checks if Docker is in the missing tools list and whether any
267+
// services that required it could use remote builds instead. Only services whose
268+
// service target actually listed Docker as required are included in the suggestion.
269+
func suggestRemoteBuild(
270+
svcTools []svcToolInfo,
271+
toolErr *tools.MissingToolErrors,
272+
) *internal.ErrorWithSuggestion {
273+
if !slices.Contains(toolErr.ToolNames, "Docker") {
274+
return nil
275+
}
276+
277+
// Find services that actually required Docker (per their service target)
278+
// and could use remoteBuild instead.
279+
var remoteBuildCapable []string
280+
for _, info := range svcTools {
281+
if !info.needsDocker {
282+
continue
283+
}
284+
remoteBuildCapable = append(remoteBuildCapable, info.svc.Name)
285+
}
286+
287+
if len(remoteBuildCapable) == 0 {
288+
return nil
289+
}
290+
291+
// Check whether the container runtime is not installed or just not running
292+
errMsg := toolErr.Error()
293+
isNotRunning := strings.Contains(errMsg, "is not running")
294+
295+
serviceList := strings.Join(remoteBuildCapable, ", ")
296+
var suggestion string
297+
if isNotRunning {
298+
suggestion = fmt.Sprintf(
299+
"Services [%s] can build on Azure instead of locally.\n"+
300+
"Set 'remoteBuild: true' under the 'docker:' section for each service in azure.yaml,\n"+
301+
"or start your container runtime (Docker/Podman).",
302+
serviceList)
303+
} else {
304+
suggestion = fmt.Sprintf(
305+
"Services [%s] can build on Azure instead of locally.\n"+
306+
"Set 'remoteBuild: true' under the 'docker:' section for each service in azure.yaml,\n"+
307+
"or install Docker (https://aka.ms/azure-dev/docker-install)\n"+
308+
"or Podman (https://aka.ms/azure-dev/podman-install).",
309+
serviceList)
310+
}
311+
312+
return &internal.ErrorWithSuggestion{
313+
Err: toolErr,
314+
Suggestion: suggestion,
315+
}
316+
}
317+
241318
func (pm *projectManager) EnsureRestoreTools(
242319
ctx context.Context,
243320
projectConfig *ProjectConfig,
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package project
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/azure/azure-dev/cli/azd/pkg/tools"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func Test_suggestRemoteBuild(t *testing.T) {
16+
dockerMissing := &tools.MissingToolErrors{
17+
ToolNames: []string{"Docker"},
18+
Errs: []error{fmt.Errorf("neither docker nor podman is installed")},
19+
}
20+
dockerNotRunning := &tools.MissingToolErrors{
21+
ToolNames: []string{"Docker"},
22+
Errs: []error{fmt.Errorf("the Docker service is not running, please start it")},
23+
}
24+
bicepMissing := &tools.MissingToolErrors{
25+
ToolNames: []string{"bicep"},
26+
Errs: []error{assert.AnError},
27+
}
28+
29+
tests := []struct {
30+
name string
31+
svcTools []svcToolInfo
32+
toolErr *tools.MissingToolErrors
33+
wantSuggestion bool
34+
wantContains string
35+
}{
36+
{
37+
name: "Service_needing_Docker_suggests",
38+
svcTools: []svcToolInfo{
39+
{svc: &ServiceConfig{Name: "api"}, needsDocker: true},
40+
},
41+
toolErr: dockerMissing,
42+
wantSuggestion: true,
43+
wantContains: "api",
44+
},
45+
{
46+
name: "Multiple_services_lists_all",
47+
svcTools: []svcToolInfo{
48+
{svc: &ServiceConfig{Name: "api"}, needsDocker: true},
49+
{svc: &ServiceConfig{Name: "web"}, needsDocker: true},
50+
},
51+
toolErr: dockerMissing,
52+
wantSuggestion: true,
53+
wantContains: "api, web",
54+
},
55+
{
56+
name: "Service_not_needing_Docker_no_suggestion",
57+
svcTools: []svcToolInfo{
58+
{svc: &ServiceConfig{Name: "api"}, needsDocker: false},
59+
},
60+
toolErr: dockerMissing,
61+
wantSuggestion: false,
62+
},
63+
{
64+
name: "Non_Docker_tool_missing_no_suggestion",
65+
svcTools: []svcToolInfo{
66+
{svc: &ServiceConfig{Name: "api"}, needsDocker: true},
67+
},
68+
toolErr: bicepMissing,
69+
wantSuggestion: false,
70+
},
71+
{
72+
name: "Mixed_services_only_Docker_ones",
73+
svcTools: []svcToolInfo{
74+
{svc: &ServiceConfig{Name: "api"}, needsDocker: true},
75+
{svc: &ServiceConfig{Name: "web"}, needsDocker: false},
76+
{svc: &ServiceConfig{Name: "worker"}, needsDocker: true},
77+
},
78+
toolErr: dockerMissing,
79+
wantSuggestion: true,
80+
wantContains: "api, worker",
81+
},
82+
{
83+
name: "Docker_not_running_suggests_start",
84+
svcTools: []svcToolInfo{
85+
{svc: &ServiceConfig{Name: "api"}, needsDocker: true},
86+
},
87+
toolErr: dockerNotRunning,
88+
wantSuggestion: true,
89+
wantContains: "start your container runtime",
90+
},
91+
{
92+
name: "Docker_not_installed_suggests_install",
93+
svcTools: []svcToolInfo{
94+
{svc: &ServiceConfig{Name: "api"}, needsDocker: true},
95+
},
96+
toolErr: dockerMissing,
97+
wantSuggestion: true,
98+
wantContains: "install Docker",
99+
},
100+
{
101+
name: "Empty_services_no_suggestion",
102+
svcTools: []svcToolInfo{},
103+
toolErr: dockerMissing,
104+
wantSuggestion: false,
105+
},
106+
}
107+
108+
for _, tt := range tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
result := suggestRemoteBuild(tt.svcTools, tt.toolErr)
111+
112+
if !tt.wantSuggestion {
113+
assert.Nil(t, result)
114+
return
115+
}
116+
117+
require.NotNil(t, result)
118+
assert.Contains(t, result.Suggestion, tt.wantContains)
119+
assert.Contains(t, result.Suggestion, "remoteBuild")
120+
})
121+
}
122+
}

cli/azd/resources/error_suggestions.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,34 @@ rules:
479479
- url: "https://learn.microsoft.com/azure/developer/azure-developer-cli/reference#azd-auth-login"
480480
title: "azd auth login reference"
481481

482+
# ============================================================================
483+
# Tool Missing Errors — Docker / Container Runtime
484+
# ============================================================================
485+
486+
- errorType: "MissingToolErrors"
487+
patterns:
488+
- "is not running"
489+
message: "The container runtime (Docker/Podman) is not running."
490+
suggestion: >-
491+
Start your container runtime, or build on Azure instead by setting
492+
'remoteBuild: true' under the 'docker:' section for each service in azure.yaml.
493+
494+
- errorType: "MissingToolErrors"
495+
patterns:
496+
- "Docker"
497+
message: "No container runtime (Docker/Podman) is installed."
498+
suggestion: >-
499+
If your services use Container Apps or AKS, you can build on Azure instead
500+
of locally. Set 'remoteBuild: true' under the 'docker:' section for each
501+
service in azure.yaml. Otherwise, install Docker or Podman.
502+
links:
503+
- url: "https://aka.ms/azure-dev/docker-install"
504+
title: "Install Docker"
505+
- url: "https://aka.ms/azure-dev/podman-install"
506+
title: "Install Podman"
507+
- url: "https://learn.microsoft.com/azure/developer/azure-developer-cli/azd-schema"
508+
title: "azure.yaml schema reference"
509+
482510
# ============================================================================
483511
# Text Pattern Rules — Specific patterns first
484512
# These are fallbacks for errors without typed Go structs.

0 commit comments

Comments
 (0)