Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ This is the Azure Developer CLI - a Go-based CLI tool for managing Azure applica
```

### Development flow
**Prerequisites:**
- [Go](https://go.dev/dl/) 1.24
- [cspell](https://github.com/streetsidesoftware/cspell)
- [golangci-lint](https://golangci-lint.run/)

If any of these tools aren't installed, install them first before proceeding.

**Build `azd` binary:**
```bash
Expand Down Expand Up @@ -61,11 +55,6 @@ When preparing a new release changelog, update `cli/azd/CHANGELOG.md` and `cli/v
Rename any existing `## 1.x.x-beta.1 (Unreleased)` section to the version being released, without the `-beta.1` and `Unreleased` parts. Do the same for `cli/version.txt`.

### Step 2: Gather commits
**IMPORTANT**: Ensure you have the latest commits from main by first running these `git fetch` commands:
```bash
git fetch --unshallow origin && git fetch origin main:refs/remotes/origin/main
```

**Find cutoff commit**:
```bash
git --no-pager log --grep="Increment CLI version" --invert-grep -n 3 --follow -p -- cli/azd/CHANGELOG.md
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: "Copilot Setup Steps"

# This workflow is used to set up the environment for GitHub Copilot coding agent.
# https://docs.github.com/copilot/customizing-copilot/customizing-the-development-environment-for-copilot-coding-agent

# Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml

jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
runs-on: ubuntu-latest

permissions:
# To clone repo
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# Fetch full history for writing changelogs
fetch-depth: 0

# Workaround for fetch-depth being overridden by coding agent orchestration
- run: git fetch --unshallow origin && git fetch origin main:refs/remotes/origin/main
continue-on-error: true

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "^1.24"

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install golangci-lint
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.0.0

- name: Install cspell
run: npm install -g cspell@8.13.1
1 change: 1 addition & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ words:
- chinacloudapi
- Codespace
- Codespaces
- containerd
- devcontainers
- extendee
- eiannone
Expand Down
32 changes: 32 additions & 0 deletions cli/azd/pkg/tools/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strconv"
"strings"

"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/blang/semver/v4"
Expand Down Expand Up @@ -118,6 +119,17 @@ func (d *Cli) Build(

_, err = d.commandRunner.Run(ctx, runArgs)
if err != nil {
// Check if this is a containerd-related error
if d.isContainerdBuildError(err) {
containerdEnabled, checkErr := d.IsContainerdEnabled(ctx)
if checkErr == nil && containerdEnabled {
return "", &internal.ErrorWithSuggestion{
Err: fmt.Errorf("building image: %w", err),
//nolint:lll
Suggestion: "Docker build failed with containerd image store enabled. Try disabling the containerd image store in Docker Desktop settings under 'Features in development' and run 'azd package' again",
}
}
}
return "", fmt.Errorf("building image: %w", err)
}

Expand Down Expand Up @@ -164,6 +176,26 @@ func (d *Cli) Inspect(ctx context.Context, imageName string, format string) (str
return out.Stdout, nil
}

// IsContainerdEnabled checks if Docker is using containerd as the image store
func (d *Cli) IsContainerdEnabled(ctx context.Context) (bool, error) {
out, err := d.executeCommand(ctx, "", "info", "-f", "{{ .DriverStatus }}")
if err != nil {
return false, fmt.Errorf("checking docker driver status: %w", err)
}

// When containerd is enabled, the output contains "driver-type io.containerd.snapshotter.v1"
return strings.Contains(out.Stdout, "driver-type io.containerd.snapshotter.v1"), nil
}

// isContainerdBuildError checks if the error is related to containerd image store issues
func (d *Cli) isContainerdBuildError(err error) bool {
errStr := err.Error()
// Check for the specific error pattern that occurs with containerd enabled
return strings.Contains(errStr, "saving image") &&
strings.Contains(errStr, "No such image") &&
strings.Contains(errStr, "pack.local/builder")
}

func (d *Cli) versionInfo() tools.VersionInfo {
return tools.VersionInfo{
MinimumVersion: semver.Version{
Expand Down
218 changes: 218 additions & 0 deletions cli/azd/pkg/tools/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@ import (
"strings"
"testing"

"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/stretchr/testify/require"
)

const mockedDockerImgId = "fake-docker-image-id"

const containerdBuildError = "ERROR: failed to build: failed to write image to the following tags: " +
"[pack.local/builder/657662746b6877776b68:latest: saving image " +
"\"pack.local/builder/657662746b6877776b68:latest\": Error response from daemon: " +
"No such image: sha256:1a3f079e7ffed5eb4c02ecf6fdcc38c8fe459b021b4803471703dbded90181c4]"

const dockerInfoContainerdDisabled = "[[Backing Filesystem extfs] [Supports d_type true] " +
"[Using metacopy false] [Native Overlay Diff true] [userxattr false]]"

func Test_DockerBuild(t *testing.T) {
cwd := "."
dockerFile := "./Dockerfile"
Expand Down Expand Up @@ -596,3 +605,212 @@ func TestSplitDockerImage(t *testing.T) {
})
}
}

func TestIsContainerdEnabled(t *testing.T) {
t.Run("ContainerdEnabled", func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
docker := NewCli(mockContext.CommandRunner)

mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker info")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
require.Equal(t, "docker", args.Cmd)
require.Equal(t, []string{"info", "-f", "{{ .DriverStatus }}"}, args.Args)

return exec.RunResult{
Stdout: "[[driver-type io.containerd.snapshotter.v1]]",
Stderr: "",
ExitCode: 0,
}, nil
})

enabled, err := docker.IsContainerdEnabled(context.Background())
require.NoError(t, err)
require.True(t, enabled)
})

t.Run("ContainerdDisabled", func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
docker := NewCli(mockContext.CommandRunner)

mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker info")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
require.Equal(t, "docker", args.Cmd)
require.Equal(t, []string{"info", "-f", "{{ .DriverStatus }}"}, args.Args)

return exec.RunResult{
Stdout: dockerInfoContainerdDisabled,
Stderr: "",
ExitCode: 0,
}, nil
})

enabled, err := docker.IsContainerdEnabled(context.Background())
require.NoError(t, err)
require.False(t, enabled)
})

t.Run("ErrorExecutingCommand", func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
docker := NewCli(mockContext.CommandRunner)

mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker info")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
ExitCode: 1,
}, errors.New("docker daemon not running")
})

enabled, err := docker.IsContainerdEnabled(context.Background())
require.Error(t, err)
require.False(t, enabled)
require.Contains(t, err.Error(), "checking docker driver status")
})
}

func TestBuildWithContainerdError(t *testing.T) {
cwd := "."
dockerFile := "./Dockerfile"
dockerContext := "../"
platform := DefaultPlatform
imageName := "IMAGE_NAME"

t.Run("ContainerdBuildErrorWithSuggestion", func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
docker := NewCli(mockContext.CommandRunner)

// Mock the docker build command to return containerd-related error
mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker build")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
ExitCode: 1,
}, errors.New(containerdBuildError)
})

// Mock the docker info command to return containerd enabled
mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker info")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
Stdout: "[[driver-type io.containerd.snapshotter.v1]]",
Stderr: "",
ExitCode: 0,
}, nil
})

_, err := docker.Build(
context.Background(),
cwd,
dockerFile,
platform,
"",
dockerContext,
imageName,
[]string{},
[]string{},
[]string{},
nil,
)

require.Error(t, err)

// Check if it's an ErrorWithSuggestion
var errWithSuggestion *internal.ErrorWithSuggestion
require.True(t, errors.As(err, &errWithSuggestion))
require.Contains(t, errWithSuggestion.Suggestion, "containerd image store")
require.Contains(t, errWithSuggestion.Suggestion, "Docker Desktop settings")
})

t.Run("ContainerdBuildErrorButContainerdCheckFails", func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
docker := NewCli(mockContext.CommandRunner)

// Mock the docker build command to return containerd-related error
mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker build")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
ExitCode: 1,
}, errors.New(containerdBuildError)
})

// Mock the docker info command to fail
mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker info")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
ExitCode: 1,
}, errors.New("docker daemon not running")
})

_, err := docker.Build(
context.Background(),
cwd,
dockerFile,
platform,
"",
dockerContext,
imageName,
[]string{},
[]string{},
[]string{},
nil,
)

require.Error(t, err)

// Should not be ErrorWithSuggestion since containerd check failed
var errWithSuggestion *internal.ErrorWithSuggestion
require.False(t, errors.As(err, &errWithSuggestion))
require.Contains(t, err.Error(), "building image")
})

t.Run("ContainerdBuildErrorButContainerdDisabled", func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
docker := NewCli(mockContext.CommandRunner)

// Mock the docker build command to return containerd-related error
mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker build")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
ExitCode: 1,
}, errors.New(containerdBuildError)
})

// Mock the docker info command to return containerd disabled
mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker info")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
Stdout: dockerInfoContainerdDisabled,
Stderr: "",
ExitCode: 0,
}, nil
})

_, err := docker.Build(
context.Background(),
cwd,
dockerFile,
platform,
"",
dockerContext,
imageName,
[]string{},
[]string{},
[]string{},
nil,
)

require.Error(t, err)

// Should not be ErrorWithSuggestion since containerd is disabled
var errWithSuggestion *internal.ErrorWithSuggestion
require.False(t, errors.As(err, &errWithSuggestion))
require.Contains(t, err.Error(), "building image")
})
}